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 74b8d396e..66d6287e2 100644 --- a/app/controlplane/api/gen/frontend/attestation/v1/crafting_state.ts +++ b/app/controlplane/api/gen/frontend/attestation/v1/crafting_state.ts @@ -311,6 +311,9 @@ export interface PolicyEvaluation_WithEntry { export interface PolicyEvaluation_Violation { subject: string; message: string; + vulnerability?: PolicyVulnerabilityFinding | undefined; + sast?: PolicySASTFinding | undefined; + licenseViolation?: PolicyLicenseViolationFinding | undefined; } export interface PolicyEvaluation_Reference { @@ -332,6 +335,69 @@ export interface PolicyEvaluationBundle { evaluations: PolicyEvaluation[]; } +/** + * Output schema for vulnerability findings from policy evaluation. + * Used when a policy declares finding_type: VULNERABILITY. + */ +export interface PolicyVulnerabilityFinding { + /** Human-readable violation description */ + message: string; + /** Vulnerability identifier (e.g., CVE-2024-1234, GHSA-xxxx) */ + externalId: string; + /** Package URL of the affected component (e.g., pkg:golang/example.com/lib@v1.0.0) */ + packagePurl: string; + /** Severity level (CRITICAL, HIGH, MEDIUM, LOW) */ + severity: string; + /** CVSS v3 score (0.0-10.0) */ + cvssV3Score: number; + /** CWE references (e.g., CWE-79, CWE-89) */ + cwes: string[]; + /** Suggested fix or upgrade path */ + recommendation: string; +} + +/** + * Output schema for SAST findings from policy evaluation. + * Used when a policy declares finding_type: SAST. + */ +export interface PolicySASTFinding { + /** Human-readable violation description */ + message: string; + /** Tool-specific rule identifier (e.g., java:S1234, go-sec:G101) */ + ruleId: string; + /** Severity level (CRITICAL, HIGH, MEDIUM, LOW) */ + severity: string; + /** File path where the issue was found */ + location: string; + /** Line number in the file */ + lineNumber: number; + /** Code snippet showing the issue */ + codeSnippet: string; + /** Suggested fix */ + recommendation: string; +} + +/** + * Output schema for license violation findings from policy evaluation. + * Used when a policy declares finding_type: LICENSE_VIOLATION. + */ +export interface PolicyLicenseViolationFinding { + /** Human-readable violation description */ + message: string; + /** Component name (e.g., "lodash") */ + componentName: string; + /** Package URL (e.g., pkg:npm/lodash@4.17.21) */ + packagePurl: string; + /** License identifier (e.g., "GPL-3.0", "AGPL-3.0") */ + licenseId: string; + /** Human-readable license name */ + licenseName: string; + /** Component version */ + componentVersion: string; + /** Suggested fix */ + recommendation: string; +} + export interface Commit { hash: string; /** Commit authors might not include email i.e "Flux <>" */ @@ -2807,7 +2873,7 @@ export const PolicyEvaluation_WithEntry = { }; function createBasePolicyEvaluation_Violation(): PolicyEvaluation_Violation { - return { subject: "", message: "" }; + return { subject: "", message: "", vulnerability: undefined, sast: undefined, licenseViolation: undefined }; } export const PolicyEvaluation_Violation = { @@ -2818,6 +2884,15 @@ export const PolicyEvaluation_Violation = { if (message.message !== "") { writer.uint32(18).string(message.message); } + if (message.vulnerability !== undefined) { + PolicyVulnerabilityFinding.encode(message.vulnerability, writer.uint32(26).fork()).ldelim(); + } + if (message.sast !== undefined) { + PolicySASTFinding.encode(message.sast, writer.uint32(34).fork()).ldelim(); + } + if (message.licenseViolation !== undefined) { + PolicyLicenseViolationFinding.encode(message.licenseViolation, writer.uint32(42).fork()).ldelim(); + } return writer; }, @@ -2842,6 +2917,27 @@ export const PolicyEvaluation_Violation = { message.message = reader.string(); continue; + case 3: + if (tag !== 26) { + break; + } + + message.vulnerability = PolicyVulnerabilityFinding.decode(reader, reader.uint32()); + continue; + case 4: + if (tag !== 34) { + break; + } + + message.sast = PolicySASTFinding.decode(reader, reader.uint32()); + continue; + case 5: + if (tag !== 42) { + break; + } + + message.licenseViolation = PolicyLicenseViolationFinding.decode(reader, reader.uint32()); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -2855,6 +2951,13 @@ export const PolicyEvaluation_Violation = { return { subject: isSet(object.subject) ? String(object.subject) : "", message: isSet(object.message) ? String(object.message) : "", + vulnerability: isSet(object.vulnerability) + ? PolicyVulnerabilityFinding.fromJSON(object.vulnerability) + : undefined, + sast: isSet(object.sast) ? PolicySASTFinding.fromJSON(object.sast) : undefined, + licenseViolation: isSet(object.licenseViolation) + ? PolicyLicenseViolationFinding.fromJSON(object.licenseViolation) + : undefined, }; }, @@ -2862,6 +2965,14 @@ export const PolicyEvaluation_Violation = { const obj: any = {}; message.subject !== undefined && (obj.subject = message.subject); message.message !== undefined && (obj.message = message.message); + message.vulnerability !== undefined && + (obj.vulnerability = message.vulnerability + ? PolicyVulnerabilityFinding.toJSON(message.vulnerability) + : undefined); + message.sast !== undefined && (obj.sast = message.sast ? PolicySASTFinding.toJSON(message.sast) : undefined); + message.licenseViolation !== undefined && (obj.licenseViolation = message.licenseViolation + ? PolicyLicenseViolationFinding.toJSON(message.licenseViolation) + : undefined); return obj; }, @@ -2873,6 +2984,15 @@ export const PolicyEvaluation_Violation = { const message = createBasePolicyEvaluation_Violation(); message.subject = object.subject ?? ""; message.message = object.message ?? ""; + message.vulnerability = (object.vulnerability !== undefined && object.vulnerability !== null) + ? PolicyVulnerabilityFinding.fromPartial(object.vulnerability) + : undefined; + message.sast = (object.sast !== undefined && object.sast !== null) + ? PolicySASTFinding.fromPartial(object.sast) + : undefined; + message.licenseViolation = (object.licenseViolation !== undefined && object.licenseViolation !== null) + ? PolicyLicenseViolationFinding.fromPartial(object.licenseViolation) + : undefined; return message; }, }; @@ -3111,6 +3231,428 @@ export const PolicyEvaluationBundle = { }, }; +function createBasePolicyVulnerabilityFinding(): PolicyVulnerabilityFinding { + return { message: "", externalId: "", packagePurl: "", severity: "", cvssV3Score: 0, cwes: [], recommendation: "" }; +} + +export const PolicyVulnerabilityFinding = { + encode(message: PolicyVulnerabilityFinding, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.message !== "") { + writer.uint32(10).string(message.message); + } + if (message.externalId !== "") { + writer.uint32(18).string(message.externalId); + } + if (message.packagePurl !== "") { + writer.uint32(26).string(message.packagePurl); + } + if (message.severity !== "") { + writer.uint32(34).string(message.severity); + } + if (message.cvssV3Score !== 0) { + writer.uint32(41).double(message.cvssV3Score); + } + for (const v of message.cwes) { + writer.uint32(50).string(v!); + } + if (message.recommendation !== "") { + writer.uint32(58).string(message.recommendation); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): PolicyVulnerabilityFinding { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBasePolicyVulnerabilityFinding(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.message = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.externalId = reader.string(); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.packagePurl = reader.string(); + continue; + case 4: + if (tag !== 34) { + break; + } + + message.severity = reader.string(); + continue; + case 5: + if (tag !== 41) { + break; + } + + message.cvssV3Score = reader.double(); + continue; + case 6: + if (tag !== 50) { + break; + } + + message.cwes.push(reader.string()); + continue; + case 7: + if (tag !== 58) { + break; + } + + message.recommendation = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): PolicyVulnerabilityFinding { + return { + message: isSet(object.message) ? String(object.message) : "", + externalId: isSet(object.externalId) ? String(object.externalId) : "", + packagePurl: isSet(object.packagePurl) ? String(object.packagePurl) : "", + severity: isSet(object.severity) ? String(object.severity) : "", + cvssV3Score: isSet(object.cvssV3Score) ? Number(object.cvssV3Score) : 0, + cwes: Array.isArray(object?.cwes) ? object.cwes.map((e: any) => String(e)) : [], + recommendation: isSet(object.recommendation) ? String(object.recommendation) : "", + }; + }, + + toJSON(message: PolicyVulnerabilityFinding): unknown { + const obj: any = {}; + message.message !== undefined && (obj.message = message.message); + message.externalId !== undefined && (obj.externalId = message.externalId); + message.packagePurl !== undefined && (obj.packagePurl = message.packagePurl); + message.severity !== undefined && (obj.severity = message.severity); + message.cvssV3Score !== undefined && (obj.cvssV3Score = message.cvssV3Score); + if (message.cwes) { + obj.cwes = message.cwes.map((e) => e); + } else { + obj.cwes = []; + } + message.recommendation !== undefined && (obj.recommendation = message.recommendation); + return obj; + }, + + create, I>>(base?: I): PolicyVulnerabilityFinding { + return PolicyVulnerabilityFinding.fromPartial(base ?? {}); + }, + + fromPartial, I>>(object: I): PolicyVulnerabilityFinding { + const message = createBasePolicyVulnerabilityFinding(); + message.message = object.message ?? ""; + message.externalId = object.externalId ?? ""; + message.packagePurl = object.packagePurl ?? ""; + message.severity = object.severity ?? ""; + message.cvssV3Score = object.cvssV3Score ?? 0; + message.cwes = object.cwes?.map((e) => e) || []; + message.recommendation = object.recommendation ?? ""; + return message; + }, +}; + +function createBasePolicySASTFinding(): PolicySASTFinding { + return { message: "", ruleId: "", severity: "", location: "", lineNumber: 0, codeSnippet: "", recommendation: "" }; +} + +export const PolicySASTFinding = { + encode(message: PolicySASTFinding, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.message !== "") { + writer.uint32(10).string(message.message); + } + if (message.ruleId !== "") { + writer.uint32(18).string(message.ruleId); + } + if (message.severity !== "") { + writer.uint32(26).string(message.severity); + } + if (message.location !== "") { + writer.uint32(34).string(message.location); + } + if (message.lineNumber !== 0) { + writer.uint32(40).int32(message.lineNumber); + } + if (message.codeSnippet !== "") { + writer.uint32(50).string(message.codeSnippet); + } + if (message.recommendation !== "") { + writer.uint32(58).string(message.recommendation); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): PolicySASTFinding { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBasePolicySASTFinding(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.message = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.ruleId = reader.string(); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.severity = reader.string(); + continue; + case 4: + if (tag !== 34) { + break; + } + + message.location = reader.string(); + continue; + case 5: + if (tag !== 40) { + break; + } + + message.lineNumber = reader.int32(); + continue; + case 6: + if (tag !== 50) { + break; + } + + message.codeSnippet = reader.string(); + continue; + case 7: + if (tag !== 58) { + break; + } + + message.recommendation = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): PolicySASTFinding { + return { + message: isSet(object.message) ? String(object.message) : "", + ruleId: isSet(object.ruleId) ? String(object.ruleId) : "", + severity: isSet(object.severity) ? String(object.severity) : "", + location: isSet(object.location) ? String(object.location) : "", + lineNumber: isSet(object.lineNumber) ? Number(object.lineNumber) : 0, + codeSnippet: isSet(object.codeSnippet) ? String(object.codeSnippet) : "", + recommendation: isSet(object.recommendation) ? String(object.recommendation) : "", + }; + }, + + toJSON(message: PolicySASTFinding): unknown { + const obj: any = {}; + message.message !== undefined && (obj.message = message.message); + message.ruleId !== undefined && (obj.ruleId = message.ruleId); + message.severity !== undefined && (obj.severity = message.severity); + message.location !== undefined && (obj.location = message.location); + message.lineNumber !== undefined && (obj.lineNumber = Math.round(message.lineNumber)); + message.codeSnippet !== undefined && (obj.codeSnippet = message.codeSnippet); + message.recommendation !== undefined && (obj.recommendation = message.recommendation); + return obj; + }, + + create, I>>(base?: I): PolicySASTFinding { + return PolicySASTFinding.fromPartial(base ?? {}); + }, + + fromPartial, I>>(object: I): PolicySASTFinding { + const message = createBasePolicySASTFinding(); + message.message = object.message ?? ""; + message.ruleId = object.ruleId ?? ""; + message.severity = object.severity ?? ""; + message.location = object.location ?? ""; + message.lineNumber = object.lineNumber ?? 0; + message.codeSnippet = object.codeSnippet ?? ""; + message.recommendation = object.recommendation ?? ""; + return message; + }, +}; + +function createBasePolicyLicenseViolationFinding(): PolicyLicenseViolationFinding { + return { + message: "", + componentName: "", + packagePurl: "", + licenseId: "", + licenseName: "", + componentVersion: "", + recommendation: "", + }; +} + +export const PolicyLicenseViolationFinding = { + encode(message: PolicyLicenseViolationFinding, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.message !== "") { + writer.uint32(10).string(message.message); + } + if (message.componentName !== "") { + writer.uint32(18).string(message.componentName); + } + if (message.packagePurl !== "") { + writer.uint32(26).string(message.packagePurl); + } + if (message.licenseId !== "") { + writer.uint32(34).string(message.licenseId); + } + if (message.licenseName !== "") { + writer.uint32(42).string(message.licenseName); + } + if (message.componentVersion !== "") { + writer.uint32(50).string(message.componentVersion); + } + if (message.recommendation !== "") { + writer.uint32(58).string(message.recommendation); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): PolicyLicenseViolationFinding { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBasePolicyLicenseViolationFinding(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.message = reader.string(); + continue; + case 2: + if (tag !== 18) { + break; + } + + message.componentName = reader.string(); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.packagePurl = reader.string(); + continue; + case 4: + if (tag !== 34) { + break; + } + + message.licenseId = reader.string(); + continue; + case 5: + if (tag !== 42) { + break; + } + + message.licenseName = reader.string(); + continue; + case 6: + if (tag !== 50) { + break; + } + + message.componentVersion = reader.string(); + continue; + case 7: + if (tag !== 58) { + break; + } + + message.recommendation = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): PolicyLicenseViolationFinding { + return { + message: isSet(object.message) ? String(object.message) : "", + componentName: isSet(object.componentName) ? String(object.componentName) : "", + packagePurl: isSet(object.packagePurl) ? String(object.packagePurl) : "", + licenseId: isSet(object.licenseId) ? String(object.licenseId) : "", + licenseName: isSet(object.licenseName) ? String(object.licenseName) : "", + componentVersion: isSet(object.componentVersion) ? String(object.componentVersion) : "", + recommendation: isSet(object.recommendation) ? String(object.recommendation) : "", + }; + }, + + toJSON(message: PolicyLicenseViolationFinding): unknown { + const obj: any = {}; + message.message !== undefined && (obj.message = message.message); + message.componentName !== undefined && (obj.componentName = message.componentName); + message.packagePurl !== undefined && (obj.packagePurl = message.packagePurl); + message.licenseId !== undefined && (obj.licenseId = message.licenseId); + message.licenseName !== undefined && (obj.licenseName = message.licenseName); + message.componentVersion !== undefined && (obj.componentVersion = message.componentVersion); + message.recommendation !== undefined && (obj.recommendation = message.recommendation); + return obj; + }, + + create, I>>(base?: I): PolicyLicenseViolationFinding { + return PolicyLicenseViolationFinding.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + object: I, + ): PolicyLicenseViolationFinding { + const message = createBasePolicyLicenseViolationFinding(); + message.message = object.message ?? ""; + message.componentName = object.componentName ?? ""; + message.packagePurl = object.packagePurl ?? ""; + message.licenseId = object.licenseId ?? ""; + message.licenseName = object.licenseName ?? ""; + message.componentVersion = object.componentVersion ?? ""; + message.recommendation = object.recommendation ?? ""; + return message; + }, +}; + function createBaseCommit(): Commit { return { hash: "", diff --git a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts index 0165cefcf..03459686b 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -533,7 +533,18 @@ export interface Metadata { name: string; description: string; annotations: { [key: string]: string }; - organization?: string | undefined; + organization?: + | string + | undefined; + /** + * Declares the structured output schema for policy violations. + * When set, the policy engine validates that violations conform to the + * corresponding proto message: + * VULNERABILITY -> attestation.v1.PolicyVulnerabilityFinding + * SAST -> attestation.v1.PolicySASTFinding + * LICENSE_VIOLATION -> attestation.v1.PolicyLicenseViolationFinding + */ + findingType?: string | undefined; } export interface Metadata_AnnotationsEntry { @@ -1833,7 +1844,7 @@ export const Policy = { }; function createBaseMetadata(): Metadata { - return { name: "", description: "", annotations: {}, organization: undefined }; + return { name: "", description: "", annotations: {}, organization: undefined, findingType: undefined }; } export const Metadata = { @@ -1850,6 +1861,9 @@ export const Metadata = { if (message.organization !== undefined) { writer.uint32(50).string(message.organization); } + if (message.findingType !== undefined) { + writer.uint32(58).string(message.findingType); + } return writer; }, @@ -1891,6 +1905,13 @@ export const Metadata = { message.organization = reader.string(); continue; + case 7: + if (tag !== 58) { + break; + } + + message.findingType = reader.string(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -1911,6 +1932,7 @@ export const Metadata = { }, {}) : {}, organization: isSet(object.organization) ? String(object.organization) : undefined, + findingType: isSet(object.findingType) ? String(object.findingType) : undefined, }; }, @@ -1925,6 +1947,7 @@ export const Metadata = { }); } message.organization !== undefined && (obj.organization = message.organization); + message.findingType !== undefined && (obj.findingType = message.findingType); return obj; }, @@ -1946,6 +1969,7 @@ export const Metadata = { {}, ); message.organization = object.organization ?? undefined; + message.findingType = object.findingType ?? undefined; return message; }, }; diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.jsonschema.json index b7a1f64a6..e7d821106 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.jsonschema.json @@ -2,12 +2,26 @@ "$id": "attestation.v1.PolicyEvaluation.Violation.jsonschema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, + "patternProperties": { + "^(license_violation)$": { + "$ref": "attestation.v1.PolicyLicenseViolationFinding.jsonschema.json" + } + }, "properties": { + "licenseViolation": { + "$ref": "attestation.v1.PolicyLicenseViolationFinding.jsonschema.json" + }, "message": { "type": "string" }, + "sast": { + "$ref": "attestation.v1.PolicySASTFinding.jsonschema.json" + }, "subject": { "type": "string" + }, + "vulnerability": { + "$ref": "attestation.v1.PolicyVulnerabilityFinding.jsonschema.json" } }, "required": [ diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.schema.json index d026aa9d8..dc57a847c 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.schema.json @@ -2,12 +2,26 @@ "$id": "attestation.v1.PolicyEvaluation.Violation.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, + "patternProperties": { + "^(licenseViolation)$": { + "$ref": "attestation.v1.PolicyLicenseViolationFinding.schema.json" + } + }, "properties": { + "license_violation": { + "$ref": "attestation.v1.PolicyLicenseViolationFinding.schema.json" + }, "message": { "type": "string" }, + "sast": { + "$ref": "attestation.v1.PolicySASTFinding.schema.json" + }, "subject": { "type": "string" + }, + "vulnerability": { + "$ref": "attestation.v1.PolicyVulnerabilityFinding.schema.json" } }, "required": [ diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyLicenseViolationFinding.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyLicenseViolationFinding.jsonschema.json new file mode 100644 index 000000000..753269b92 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyLicenseViolationFinding.jsonschema.json @@ -0,0 +1,65 @@ +{ + "$id": "attestation.v1.PolicyLicenseViolationFinding.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Output schema for license violation findings from policy evaluation.\n Used when a policy declares finding_type: LICENSE_VIOLATION.", + "patternProperties": { + "^(component_name)$": { + "description": "Component name (e.g., \"lodash\")", + "type": "string" + }, + "^(component_version)$": { + "description": "Component version", + "type": "string" + }, + "^(license_id)$": { + "description": "License identifier (e.g., \"GPL-3.0\", \"AGPL-3.0\")", + "type": "string" + }, + "^(license_name)$": { + "description": "Human-readable license name", + "type": "string" + }, + "^(package_purl)$": { + "description": "Package URL (e.g., pkg:npm/lodash@4.17.21)", + "type": "string" + } + }, + "properties": { + "componentName": { + "description": "Component name (e.g., \"lodash\")", + "type": "string" + }, + "componentVersion": { + "description": "Component version", + "type": "string" + }, + "licenseId": { + "description": "License identifier (e.g., \"GPL-3.0\", \"AGPL-3.0\")", + "type": "string" + }, + "licenseName": { + "description": "Human-readable license name", + "type": "string" + }, + "message": { + "description": "Human-readable violation description", + "type": "string" + }, + "packagePurl": { + "description": "Package URL (e.g., pkg:npm/lodash@4.17.21)", + "type": "string" + }, + "recommendation": { + "description": "Suggested fix", + "type": "string" + } + }, + "required": [ + "message", + "component_name", + "license_id" + ], + "title": "Policy License Violation Finding", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyLicenseViolationFinding.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyLicenseViolationFinding.schema.json new file mode 100644 index 000000000..35f0445c0 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyLicenseViolationFinding.schema.json @@ -0,0 +1,65 @@ +{ + "$id": "attestation.v1.PolicyLicenseViolationFinding.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Output schema for license violation findings from policy evaluation.\n Used when a policy declares finding_type: LICENSE_VIOLATION.", + "patternProperties": { + "^(componentName)$": { + "description": "Component name (e.g., \"lodash\")", + "type": "string" + }, + "^(componentVersion)$": { + "description": "Component version", + "type": "string" + }, + "^(licenseId)$": { + "description": "License identifier (e.g., \"GPL-3.0\", \"AGPL-3.0\")", + "type": "string" + }, + "^(licenseName)$": { + "description": "Human-readable license name", + "type": "string" + }, + "^(packagePurl)$": { + "description": "Package URL (e.g., pkg:npm/lodash@4.17.21)", + "type": "string" + } + }, + "properties": { + "component_name": { + "description": "Component name (e.g., \"lodash\")", + "type": "string" + }, + "component_version": { + "description": "Component version", + "type": "string" + }, + "license_id": { + "description": "License identifier (e.g., \"GPL-3.0\", \"AGPL-3.0\")", + "type": "string" + }, + "license_name": { + "description": "Human-readable license name", + "type": "string" + }, + "message": { + "description": "Human-readable violation description", + "type": "string" + }, + "package_purl": { + "description": "Package URL (e.g., pkg:npm/lodash@4.17.21)", + "type": "string" + }, + "recommendation": { + "description": "Suggested fix", + "type": "string" + } + }, + "required": [ + "message", + "component_name", + "license_id" + ], + "title": "Policy License Violation Finding", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicySASTFinding.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicySASTFinding.jsonschema.json new file mode 100644 index 000000000..cc9bbcffc --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicySASTFinding.jsonschema.json @@ -0,0 +1,62 @@ +{ + "$id": "attestation.v1.PolicySASTFinding.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Output schema for SAST findings from policy evaluation.\n Used when a policy declares finding_type: SAST.", + "patternProperties": { + "^(code_snippet)$": { + "description": "Code snippet showing the issue", + "type": "string" + }, + "^(line_number)$": { + "description": "Line number in the file", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "^(rule_id)$": { + "description": "Tool-specific rule identifier (e.g., java:S1234, go-sec:G101)", + "type": "string" + } + }, + "properties": { + "codeSnippet": { + "description": "Code snippet showing the issue", + "type": "string" + }, + "lineNumber": { + "description": "Line number in the file", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "location": { + "description": "File path where the issue was found", + "type": "string" + }, + "message": { + "description": "Human-readable violation description", + "type": "string" + }, + "recommendation": { + "description": "Suggested fix", + "type": "string" + }, + "ruleId": { + "description": "Tool-specific rule identifier (e.g., java:S1234, go-sec:G101)", + "type": "string" + }, + "severity": { + "description": "Severity level (CRITICAL, HIGH, MEDIUM, LOW)", + "type": "string" + } + }, + "required": [ + "message", + "rule_id", + "severity", + "location" + ], + "title": "PolicySAST Finding", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicySASTFinding.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicySASTFinding.schema.json new file mode 100644 index 000000000..01d666207 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicySASTFinding.schema.json @@ -0,0 +1,62 @@ +{ + "$id": "attestation.v1.PolicySASTFinding.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Output schema for SAST findings from policy evaluation.\n Used when a policy declares finding_type: SAST.", + "patternProperties": { + "^(codeSnippet)$": { + "description": "Code snippet showing the issue", + "type": "string" + }, + "^(lineNumber)$": { + "description": "Line number in the file", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "^(ruleId)$": { + "description": "Tool-specific rule identifier (e.g., java:S1234, go-sec:G101)", + "type": "string" + } + }, + "properties": { + "code_snippet": { + "description": "Code snippet showing the issue", + "type": "string" + }, + "line_number": { + "description": "Line number in the file", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "location": { + "description": "File path where the issue was found", + "type": "string" + }, + "message": { + "description": "Human-readable violation description", + "type": "string" + }, + "recommendation": { + "description": "Suggested fix", + "type": "string" + }, + "rule_id": { + "description": "Tool-specific rule identifier (e.g., java:S1234, go-sec:G101)", + "type": "string" + }, + "severity": { + "description": "Severity level (CRITICAL, HIGH, MEDIUM, LOW)", + "type": "string" + } + }, + "required": [ + "message", + "rule_id", + "severity", + "location" + ], + "title": "PolicySAST Finding", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyVulnerabilityFinding.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyVulnerabilityFinding.jsonschema.json new file mode 100644 index 000000000..7b86551c1 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyVulnerabilityFinding.jsonschema.json @@ -0,0 +1,81 @@ +{ + "$id": "attestation.v1.PolicyVulnerabilityFinding.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Output schema for vulnerability findings from policy evaluation.\n Used when a policy declares finding_type: VULNERABILITY.", + "patternProperties": { + "^(cvss_v3_score)$": { + "anyOf": [ + { + "maximum": 10, + "minimum": 0, + "type": "number" + }, + { + "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "type": "string" + } + ], + "description": "CVSS v3 score (0.0-10.0)" + }, + "^(external_id)$": { + "description": "Vulnerability identifier (e.g., CVE-2024-1234, GHSA-xxxx)", + "type": "string" + }, + "^(package_purl)$": { + "description": "Package URL of the affected component (e.g., pkg:golang/example.com/lib@v1.0.0)", + "type": "string" + } + }, + "properties": { + "cvssV3Score": { + "anyOf": [ + { + "maximum": 10, + "minimum": 0, + "type": "number" + }, + { + "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "type": "string" + } + ], + "description": "CVSS v3 score (0.0-10.0)" + }, + "cwes": { + "description": "CWE references (e.g., CWE-79, CWE-89)", + "items": { + "type": "string" + }, + "type": "array" + }, + "externalId": { + "description": "Vulnerability identifier (e.g., CVE-2024-1234, GHSA-xxxx)", + "type": "string" + }, + "message": { + "description": "Human-readable violation description", + "type": "string" + }, + "packagePurl": { + "description": "Package URL of the affected component (e.g., pkg:golang/example.com/lib@v1.0.0)", + "type": "string" + }, + "recommendation": { + "description": "Suggested fix or upgrade path", + "type": "string" + }, + "severity": { + "description": "Severity level (CRITICAL, HIGH, MEDIUM, LOW)", + "type": "string" + } + }, + "required": [ + "message", + "external_id", + "package_purl", + "severity" + ], + "title": "Policy Vulnerability Finding", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyVulnerabilityFinding.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyVulnerabilityFinding.schema.json new file mode 100644 index 000000000..439bd26da --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyVulnerabilityFinding.schema.json @@ -0,0 +1,81 @@ +{ + "$id": "attestation.v1.PolicyVulnerabilityFinding.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Output schema for vulnerability findings from policy evaluation.\n Used when a policy declares finding_type: VULNERABILITY.", + "patternProperties": { + "^(cvssV3Score)$": { + "anyOf": [ + { + "maximum": 10, + "minimum": 0, + "type": "number" + }, + { + "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "type": "string" + } + ], + "description": "CVSS v3 score (0.0-10.0)" + }, + "^(externalId)$": { + "description": "Vulnerability identifier (e.g., CVE-2024-1234, GHSA-xxxx)", + "type": "string" + }, + "^(packagePurl)$": { + "description": "Package URL of the affected component (e.g., pkg:golang/example.com/lib@v1.0.0)", + "type": "string" + } + }, + "properties": { + "cvss_v3_score": { + "anyOf": [ + { + "maximum": 10, + "minimum": 0, + "type": "number" + }, + { + "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", + "type": "string" + } + ], + "description": "CVSS v3 score (0.0-10.0)" + }, + "cwes": { + "description": "CWE references (e.g., CWE-79, CWE-89)", + "items": { + "type": "string" + }, + "type": "array" + }, + "external_id": { + "description": "Vulnerability identifier (e.g., CVE-2024-1234, GHSA-xxxx)", + "type": "string" + }, + "message": { + "description": "Human-readable violation description", + "type": "string" + }, + "package_purl": { + "description": "Package URL of the affected component (e.g., pkg:golang/example.com/lib@v1.0.0)", + "type": "string" + }, + "recommendation": { + "description": "Suggested fix or upgrade path", + "type": "string" + }, + "severity": { + "description": "Severity level (CRITICAL, HIGH, MEDIUM, LOW)", + "type": "string" + } + }, + "required": [ + "message", + "external_id", + "package_purl", + "severity" + ], + "title": "Policy Vulnerability Finding", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Metadata.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Metadata.jsonschema.json index e4e9ab76a..739d05dd5 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Metadata.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Metadata.jsonschema.json @@ -2,6 +2,17 @@ "$id": "workflowcontract.v1.Metadata.jsonschema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, + "patternProperties": { + "^(finding_type)$": { + "description": "Declares the structured output schema for policy violations.\n When set, the policy engine validates that violations conform to the\n corresponding proto message:\n VULNERABILITY -\u003e attestation.v1.PolicyVulnerabilityFinding\n SAST -\u003e attestation.v1.PolicySASTFinding\n LICENSE_VIOLATION -\u003e attestation.v1.PolicyLicenseViolationFinding", + "enum": [ + "VULNERABILITY", + "SAST", + "LICENSE_VIOLATION" + ], + "type": "string" + } + }, "properties": { "annotations": { "additionalProperties": { @@ -15,6 +26,15 @@ "description": { "type": "string" }, + "findingType": { + "description": "Declares the structured output schema for policy violations.\n When set, the policy engine validates that violations conform to the\n corresponding proto message:\n VULNERABILITY -\u003e attestation.v1.PolicyVulnerabilityFinding\n SAST -\u003e attestation.v1.PolicySASTFinding\n LICENSE_VIOLATION -\u003e attestation.v1.PolicyLicenseViolationFinding", + "enum": [ + "VULNERABILITY", + "SAST", + "LICENSE_VIOLATION" + ], + "type": "string" + }, "name": { "description": "the name of the policy", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Metadata.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Metadata.schema.json index 8acfeb1e1..bad154378 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Metadata.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.Metadata.schema.json @@ -2,6 +2,17 @@ "$id": "workflowcontract.v1.Metadata.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, + "patternProperties": { + "^(findingType)$": { + "description": "Declares the structured output schema for policy violations.\n When set, the policy engine validates that violations conform to the\n corresponding proto message:\n VULNERABILITY -\u003e attestation.v1.PolicyVulnerabilityFinding\n SAST -\u003e attestation.v1.PolicySASTFinding\n LICENSE_VIOLATION -\u003e attestation.v1.PolicyLicenseViolationFinding", + "enum": [ + "VULNERABILITY", + "SAST", + "LICENSE_VIOLATION" + ], + "type": "string" + } + }, "properties": { "annotations": { "additionalProperties": { @@ -15,6 +26,15 @@ "description": { "type": "string" }, + "finding_type": { + "description": "Declares the structured output schema for policy violations.\n When set, the policy engine validates that violations conform to the\n corresponding proto message:\n VULNERABILITY -\u003e attestation.v1.PolicyVulnerabilityFinding\n SAST -\u003e attestation.v1.PolicySASTFinding\n LICENSE_VIOLATION -\u003e attestation.v1.PolicyLicenseViolationFinding", + "enum": [ + "VULNERABILITY", + "SAST", + "LICENSE_VIOLATION" + ], + "type": "string" + }, "name": { "description": "the name of the policy", "type": "string" diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go index 41dfac138..3b885435f 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -910,10 +910,18 @@ func (x *Policy) GetSpec() *PolicySpec { type Metadata struct { state protoimpl.MessageState `protogen:"open.v1"` // the name of the policy - Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` - Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` - Annotations map[string]string `protobuf:"bytes,5,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Organization *string `protobuf:"bytes,6,opt,name=organization,proto3,oneof" json:"organization,omitempty"` + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + Annotations map[string]string `protobuf:"bytes,5,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Organization *string `protobuf:"bytes,6,opt,name=organization,proto3,oneof" json:"organization,omitempty"` + // Declares the structured output schema for policy violations. + // When set, the policy engine validates that violations conform to the + // corresponding proto message: + // + // VULNERABILITY -> attestation.v1.PolicyVulnerabilityFinding + // SAST -> attestation.v1.PolicySASTFinding + // LICENSE_VIOLATION -> attestation.v1.PolicyLicenseViolationFinding + FindingType *string `protobuf:"bytes,7,opt,name=finding_type,json=findingType,proto3,oneof" json:"finding_type,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -976,6 +984,13 @@ func (x *Metadata) GetOrganization() string { return "" } +func (x *Metadata) GetFindingType() string { + if x != nil && x.FindingType != nil { + return *x.FindingType + } + return "" +} + type PolicySpec struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Source: @@ -2035,17 +2050,19 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "r\b\n" + "\x06PolicyR\x04kind\x12A\n" + "\bmetadata\x18\x03 \x01(\v2\x1d.workflowcontract.v1.MetadataB\x06\xbaH\x03\xc8\x01\x01R\bmetadata\x12;\n" + - "\x04spec\x18\x04 \x01(\v2\x1f.workflowcontract.v1.PolicySpecB\x06\xbaH\x03\xc8\x01\x01R\x04spec\"\x92\x03\n" + + "\x04spec\x18\x04 \x01(\v2\x1f.workflowcontract.v1.PolicySpecB\x06\xbaH\x03\xc8\x01\x01R\x04spec\"\xfa\x03\n" + "\bMetadata\x12\x97\x01\n" + "\x04name\x18\x03 \x01(\tB\x82\x01\xbaH\x7f\xba\x01|\n" + "\rname.dns-1123\x12:must contain only lowercase letters, numbers, and hyphens.\x1a/this.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')R\x04name\x12 \n" + "\vdescription\x18\x04 \x01(\tR\vdescription\x12P\n" + "\vannotations\x18\x05 \x03(\v2..workflowcontract.v1.Metadata.AnnotationsEntryR\vannotations\x12'\n" + - "\forganization\x18\x06 \x01(\tH\x00R\forganization\x88\x01\x01\x1a>\n" + + "\forganization\x18\x06 \x01(\tH\x00R\forganization\x88\x01\x01\x12U\n" + + "\ffinding_type\x18\a \x01(\tB-\xbaH*r(R\rVULNERABILITYR\x04SASTR\x11LICENSE_VIOLATIONH\x01R\vfindingType\x88\x01\x01\x1a>\n" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x0f\n" + - "\r_organization\"\xf8\x03\n" + + "\r_organizationB\x0f\n" + + "\r_finding_type\"\xf8\x03\n" + "\n" + "PolicySpec\x12\x18\n" + "\x04path\x18\x01 \x01(\tB\x02\x18\x01H\x00R\x04path\x12 \n" + diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto index b92e4d2a7..bb532e5d5 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -279,6 +279,20 @@ message Metadata { string description = 4; map annotations = 5; optional string organization = 6; + + // Declares the structured output schema for policy violations. + // When set, the policy engine validates that violations conform to the + // corresponding proto message: + // VULNERABILITY -> attestation.v1.PolicyVulnerabilityFinding + // SAST -> attestation.v1.PolicySASTFinding + // LICENSE_VIOLATION -> attestation.v1.PolicyLicenseViolationFinding + optional string finding_type = 7 [(buf.validate.field).string = { + in: [ + "VULNERABILITY", + "SAST", + "LICENSE_VIOLATION" + ] + }]; } message PolicySpec { diff --git a/go.mod b/go.mod index 2c2a4590c..18bc349fd 100644 --- a/go.mod +++ b/go.mod @@ -60,8 +60,8 @@ require ( ) require ( - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 - buf.build/go/protovalidate v1.1.0 + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 + buf.build/go/protovalidate v1.1.3 buf.build/go/protoyaml v0.6.0 cloud.google.com/go/storage v1.57.1 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 @@ -201,7 +201,7 @@ require ( github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect - github.com/google/cel-go v0.26.1 // indirect + github.com/google/cel-go v0.27.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-github/v73 v73.0.0 // indirect github.com/google/go-tpm v0.9.8 // indirect @@ -273,7 +273,6 @@ require ( github.com/sorairolake/lzip-go v0.3.8 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect - github.com/stoewer/go-strcase v1.3.1 // indirect github.com/styrainc/roast v0.15.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tchap/go-patricia/v2 v2.3.3 // indirect diff --git a/go.sum b/go.sum index e3fe506eb..7ab6f1a5b 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,10 @@ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeX al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= ariga.io/atlas v0.36.2-0.20250730182955-2c6300d0a3e1 h1:NPPfBaVZgz4LKBCIc0FbMogCjvXN+yGf7CZwotOwJo8= ariga.io/atlas v0.36.2-0.20250730182955-2c6300d0a3e1/go.mod h1:Ex5l1xHsnWQUc3wYnrJ9gD7RUEzG76P7ZRQp8wNr0wc= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= -buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY= -buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 h1:PMmTMyvHScV9Mn8wc6ASge9uRcHy0jtqPd+fM35LmsQ= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= +buf.build/go/protovalidate v1.1.3 h1:m2GVEgQWd7rk+vIoAZ+f0ygGjvQTuqPQapBBdcpWVPE= +buf.build/go/protovalidate v1.1.3/go.mod h1:9XIuohWz+kj+9JVn3WQneHA5LZP50mjvneZMnbLkiIE= buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w= buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q= cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= @@ -615,8 +615,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= -github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= +github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A= github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= @@ -1275,8 +1275,6 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= -github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= -github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= 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 4962f1c2a..292493eae 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state.pb.go +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state.pb.go @@ -152,7 +152,7 @@ func (x Commit_CommitVerification_VerificationStatus) Number() protoreflect.Enum // Deprecated: Use Commit_CommitVerification_VerificationStatus.Descriptor instead. func (Commit_CommitVerification_VerificationStatus) EnumDescriptor() ([]byte, []int) { - return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{4, 1, 0} + return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{7, 1, 0} } type Attestation struct { @@ -673,6 +673,309 @@ func (x *PolicyEvaluationBundle) GetEvaluations() []*PolicyEvaluation { return nil } +// Output schema for vulnerability findings from policy evaluation. +// Used when a policy declares finding_type: VULNERABILITY. +type PolicyVulnerabilityFinding struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Human-readable violation description + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + // Vulnerability identifier (e.g., CVE-2024-1234, GHSA-xxxx) + ExternalId string `protobuf:"bytes,2,opt,name=external_id,json=externalId,proto3" json:"external_id,omitempty"` + // Package URL of the affected component (e.g., pkg:golang/example.com/lib@v1.0.0) + PackagePurl string `protobuf:"bytes,3,opt,name=package_purl,json=packagePurl,proto3" json:"package_purl,omitempty"` + // Severity level (CRITICAL, HIGH, MEDIUM, LOW) + Severity string `protobuf:"bytes,4,opt,name=severity,proto3" json:"severity,omitempty"` + // CVSS v3 score (0.0-10.0) + CvssV3Score float64 `protobuf:"fixed64,5,opt,name=cvss_v3_score,json=cvssV3Score,proto3" json:"cvss_v3_score,omitempty"` + // CWE references (e.g., CWE-79, CWE-89) + Cwes []string `protobuf:"bytes,6,rep,name=cwes,proto3" json:"cwes,omitempty"` + // Suggested fix or upgrade path + Recommendation string `protobuf:"bytes,7,opt,name=recommendation,proto3" json:"recommendation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PolicyVulnerabilityFinding) Reset() { + *x = PolicyVulnerabilityFinding{} + mi := &file_attestation_v1_crafting_state_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PolicyVulnerabilityFinding) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PolicyVulnerabilityFinding) ProtoMessage() {} + +func (x *PolicyVulnerabilityFinding) ProtoReflect() protoreflect.Message { + mi := &file_attestation_v1_crafting_state_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PolicyVulnerabilityFinding.ProtoReflect.Descriptor instead. +func (*PolicyVulnerabilityFinding) Descriptor() ([]byte, []int) { + return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{4} +} + +func (x *PolicyVulnerabilityFinding) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *PolicyVulnerabilityFinding) GetExternalId() string { + if x != nil { + return x.ExternalId + } + return "" +} + +func (x *PolicyVulnerabilityFinding) GetPackagePurl() string { + if x != nil { + return x.PackagePurl + } + return "" +} + +func (x *PolicyVulnerabilityFinding) GetSeverity() string { + if x != nil { + return x.Severity + } + return "" +} + +func (x *PolicyVulnerabilityFinding) GetCvssV3Score() float64 { + if x != nil { + return x.CvssV3Score + } + return 0 +} + +func (x *PolicyVulnerabilityFinding) GetCwes() []string { + if x != nil { + return x.Cwes + } + return nil +} + +func (x *PolicyVulnerabilityFinding) GetRecommendation() string { + if x != nil { + return x.Recommendation + } + return "" +} + +// Output schema for SAST findings from policy evaluation. +// Used when a policy declares finding_type: SAST. +type PolicySASTFinding struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Human-readable violation description + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + // Tool-specific rule identifier (e.g., java:S1234, go-sec:G101) + RuleId string `protobuf:"bytes,2,opt,name=rule_id,json=ruleId,proto3" json:"rule_id,omitempty"` + // Severity level (CRITICAL, HIGH, MEDIUM, LOW) + Severity string `protobuf:"bytes,3,opt,name=severity,proto3" json:"severity,omitempty"` + // File path where the issue was found + Location string `protobuf:"bytes,4,opt,name=location,proto3" json:"location,omitempty"` + // Line number in the file + LineNumber int32 `protobuf:"varint,5,opt,name=line_number,json=lineNumber,proto3" json:"line_number,omitempty"` + // Code snippet showing the issue + 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 +} + +func (x *PolicySASTFinding) Reset() { + *x = PolicySASTFinding{} + mi := &file_attestation_v1_crafting_state_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PolicySASTFinding) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PolicySASTFinding) ProtoMessage() {} + +func (x *PolicySASTFinding) ProtoReflect() protoreflect.Message { + mi := &file_attestation_v1_crafting_state_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PolicySASTFinding.ProtoReflect.Descriptor instead. +func (*PolicySASTFinding) Descriptor() ([]byte, []int) { + return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{5} +} + +func (x *PolicySASTFinding) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *PolicySASTFinding) GetRuleId() string { + if x != nil { + return x.RuleId + } + return "" +} + +func (x *PolicySASTFinding) GetSeverity() string { + if x != nil { + return x.Severity + } + return "" +} + +func (x *PolicySASTFinding) GetLocation() string { + if x != nil { + return x.Location + } + return "" +} + +func (x *PolicySASTFinding) GetLineNumber() int32 { + if x != nil { + return x.LineNumber + } + return 0 +} + +func (x *PolicySASTFinding) GetCodeSnippet() string { + if x != nil { + return x.CodeSnippet + } + return "" +} + +func (x *PolicySASTFinding) GetRecommendation() string { + if x != nil { + return x.Recommendation + } + return "" +} + +// Output schema for license violation findings from policy evaluation. +// Used when a policy declares finding_type: LICENSE_VIOLATION. +type PolicyLicenseViolationFinding struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Human-readable violation description + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + // Component name (e.g., "lodash") + ComponentName string `protobuf:"bytes,2,opt,name=component_name,json=componentName,proto3" json:"component_name,omitempty"` + // Package URL (e.g., pkg:npm/lodash@4.17.21) + PackagePurl string `protobuf:"bytes,3,opt,name=package_purl,json=packagePurl,proto3" json:"package_purl,omitempty"` + // License identifier (e.g., "GPL-3.0", "AGPL-3.0") + LicenseId string `protobuf:"bytes,4,opt,name=license_id,json=licenseId,proto3" json:"license_id,omitempty"` + // Human-readable license name + LicenseName string `protobuf:"bytes,5,opt,name=license_name,json=licenseName,proto3" json:"license_name,omitempty"` + // Component version + ComponentVersion string `protobuf:"bytes,6,opt,name=component_version,json=componentVersion,proto3" json:"component_version,omitempty"` + // Suggested fix + Recommendation string `protobuf:"bytes,7,opt,name=recommendation,proto3" json:"recommendation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PolicyLicenseViolationFinding) Reset() { + *x = PolicyLicenseViolationFinding{} + mi := &file_attestation_v1_crafting_state_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PolicyLicenseViolationFinding) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PolicyLicenseViolationFinding) ProtoMessage() {} + +func (x *PolicyLicenseViolationFinding) ProtoReflect() protoreflect.Message { + mi := &file_attestation_v1_crafting_state_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PolicyLicenseViolationFinding.ProtoReflect.Descriptor instead. +func (*PolicyLicenseViolationFinding) Descriptor() ([]byte, []int) { + return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{6} +} + +func (x *PolicyLicenseViolationFinding) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *PolicyLicenseViolationFinding) GetComponentName() string { + if x != nil { + return x.ComponentName + } + return "" +} + +func (x *PolicyLicenseViolationFinding) GetPackagePurl() string { + if x != nil { + return x.PackagePurl + } + return "" +} + +func (x *PolicyLicenseViolationFinding) GetLicenseId() string { + if x != nil { + return x.LicenseId + } + return "" +} + +func (x *PolicyLicenseViolationFinding) GetLicenseName() string { + if x != nil { + return x.LicenseName + } + return "" +} + +func (x *PolicyLicenseViolationFinding) GetComponentVersion() string { + if x != nil { + return x.ComponentVersion + } + return "" +} + +func (x *PolicyLicenseViolationFinding) GetRecommendation() string { + if x != nil { + return x.Recommendation + } + return "" +} + type Commit struct { state protoimpl.MessageState `protogen:"open.v1"` Hash string `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` @@ -691,7 +994,7 @@ type Commit struct { func (x *Commit) Reset() { *x = Commit{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[4] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -703,7 +1006,7 @@ func (x *Commit) String() string { func (*Commit) ProtoMessage() {} func (x *Commit) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[4] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -716,7 +1019,7 @@ func (x *Commit) ProtoReflect() protoreflect.Message { // Deprecated: Use Commit.ProtoReflect.Descriptor instead. func (*Commit) Descriptor() ([]byte, []int) { - return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{4} + return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{7} } func (x *Commit) GetHash() string { @@ -793,7 +1096,7 @@ type CraftingState struct { func (x *CraftingState) Reset() { *x = CraftingState{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[5] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -805,7 +1108,7 @@ func (x *CraftingState) String() string { func (*CraftingState) ProtoMessage() {} func (x *CraftingState) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[5] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -818,7 +1121,7 @@ func (x *CraftingState) ProtoReflect() protoreflect.Message { // Deprecated: Use CraftingState.ProtoReflect.Descriptor instead. func (*CraftingState) Descriptor() ([]byte, []int) { - return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{5} + return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{8} } func (x *CraftingState) GetSchema() isCraftingState_Schema { @@ -907,7 +1210,7 @@ type WorkflowMetadata struct { func (x *WorkflowMetadata) Reset() { *x = WorkflowMetadata{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[6] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -919,7 +1222,7 @@ func (x *WorkflowMetadata) String() string { func (*WorkflowMetadata) ProtoMessage() {} func (x *WorkflowMetadata) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[6] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -932,7 +1235,7 @@ func (x *WorkflowMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkflowMetadata.ProtoReflect.Descriptor instead. func (*WorkflowMetadata) Descriptor() ([]byte, []int) { - return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{6} + return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{9} } func (x *WorkflowMetadata) GetName() string { @@ -1018,7 +1321,7 @@ type ProjectVersion struct { func (x *ProjectVersion) Reset() { *x = ProjectVersion{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[7] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1030,7 +1333,7 @@ func (x *ProjectVersion) String() string { func (*ProjectVersion) ProtoMessage() {} func (x *ProjectVersion) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[7] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1043,7 +1346,7 @@ func (x *ProjectVersion) ProtoReflect() protoreflect.Message { // Deprecated: Use ProjectVersion.ProtoReflect.Descriptor instead. func (*ProjectVersion) Descriptor() ([]byte, []int) { - return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{7} + return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{10} } func (x *ProjectVersion) GetVersion() string { @@ -1091,7 +1394,7 @@ type ResourceDescriptor struct { func (x *ResourceDescriptor) Reset() { *x = ResourceDescriptor{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[8] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1103,7 +1406,7 @@ func (x *ResourceDescriptor) String() string { func (*ResourceDescriptor) ProtoMessage() {} func (x *ResourceDescriptor) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[8] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1116,7 +1419,7 @@ func (x *ResourceDescriptor) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceDescriptor.ProtoReflect.Descriptor instead. func (*ResourceDescriptor) Descriptor() ([]byte, []int) { - return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{8} + return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{11} } func (x *ResourceDescriptor) GetName() string { @@ -1195,7 +1498,7 @@ type Attestation_Material struct { func (x *Attestation_Material) Reset() { *x = Attestation_Material{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[11] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1207,7 +1510,7 @@ func (x *Attestation_Material) String() string { func (*Attestation_Material) ProtoMessage() {} func (x *Attestation_Material) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[11] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1361,7 +1664,7 @@ type Attestation_Auth struct { func (x *Attestation_Auth) Reset() { *x = Attestation_Auth{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[13] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1373,7 +1676,7 @@ func (x *Attestation_Auth) String() string { func (*Attestation_Auth) ProtoMessage() {} func (x *Attestation_Auth) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[13] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1417,7 +1720,7 @@ type Attestation_CASBackend struct { func (x *Attestation_CASBackend) Reset() { *x = Attestation_CASBackend{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[14] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1429,7 +1732,7 @@ func (x *Attestation_CASBackend) String() string { func (*Attestation_CASBackend) ProtoMessage() {} func (x *Attestation_CASBackend) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[14] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1478,7 +1781,7 @@ type Attestation_SigningOptions struct { func (x *Attestation_SigningOptions) Reset() { *x = Attestation_SigningOptions{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[15] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1490,7 +1793,7 @@ func (x *Attestation_SigningOptions) String() string { func (*Attestation_SigningOptions) ProtoMessage() {} func (x *Attestation_SigningOptions) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[15] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1535,7 +1838,7 @@ type Attestation_Material_KeyVal struct { func (x *Attestation_Material_KeyVal) Reset() { *x = Attestation_Material_KeyVal{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[17] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1547,7 +1850,7 @@ func (x *Attestation_Material_KeyVal) String() string { func (*Attestation_Material_KeyVal) ProtoMessage() {} func (x *Attestation_Material_KeyVal) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[17] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1613,7 +1916,7 @@ type Attestation_Material_ContainerImage struct { func (x *Attestation_Material_ContainerImage) Reset() { *x = Attestation_Material_ContainerImage{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[18] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1625,7 +1928,7 @@ func (x *Attestation_Material_ContainerImage) String() string { func (*Attestation_Material_ContainerImage) ProtoMessage() {} func (x *Attestation_Material_ContainerImage) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[18] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1727,7 +2030,7 @@ type Attestation_Material_Artifact struct { func (x *Attestation_Material_Artifact) Reset() { *x = Attestation_Material_Artifact{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[19] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1739,7 +2042,7 @@ func (x *Attestation_Material_Artifact) String() string { func (*Attestation_Material_Artifact) ProtoMessage() {} func (x *Attestation_Material_Artifact) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[19] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1803,7 +2106,7 @@ type Attestation_Material_SBOMArtifact struct { func (x *Attestation_Material_SBOMArtifact) Reset() { *x = Attestation_Material_SBOMArtifact{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[20] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1815,7 +2118,7 @@ func (x *Attestation_Material_SBOMArtifact) String() string { func (*Attestation_Material_SBOMArtifact) ProtoMessage() {} func (x *Attestation_Material_SBOMArtifact) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[20] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1860,7 +2163,7 @@ type Attestation_Material_SBOMArtifact_MainComponent struct { func (x *Attestation_Material_SBOMArtifact_MainComponent) Reset() { *x = Attestation_Material_SBOMArtifact_MainComponent{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[21] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1872,7 +2175,7 @@ func (x *Attestation_Material_SBOMArtifact_MainComponent) String() string { func (*Attestation_Material_SBOMArtifact_MainComponent) ProtoMessage() {} func (x *Attestation_Material_SBOMArtifact_MainComponent) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[21] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1910,16 +2213,26 @@ func (x *Attestation_Material_SBOMArtifact_MainComponent) GetKind() string { } type PolicyEvaluation_Violation struct { - state protoimpl.MessageState `protogen:"open.v1"` - Subject string `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"` - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Subject string `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + // Structured finding data, validated against the policy's declared finding_type. + // Only populated in the CAS-stored PolicyEvaluationBundle, not in the + // inline attestation predicate. + // + // Types that are valid to be assigned to Finding: + // + // *PolicyEvaluation_Violation_Vulnerability + // *PolicyEvaluation_Violation_Sast + // *PolicyEvaluation_Violation_LicenseViolation + Finding isPolicyEvaluation_Violation_Finding `protobuf_oneof:"finding"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *PolicyEvaluation_Violation) Reset() { *x = PolicyEvaluation_Violation{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[24] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1931,7 +2244,7 @@ func (x *PolicyEvaluation_Violation) String() string { func (*PolicyEvaluation_Violation) ProtoMessage() {} func (x *PolicyEvaluation_Violation) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[24] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1961,6 +2274,62 @@ func (x *PolicyEvaluation_Violation) GetMessage() string { return "" } +func (x *PolicyEvaluation_Violation) GetFinding() isPolicyEvaluation_Violation_Finding { + if x != nil { + return x.Finding + } + return nil +} + +func (x *PolicyEvaluation_Violation) GetVulnerability() *PolicyVulnerabilityFinding { + if x != nil { + if x, ok := x.Finding.(*PolicyEvaluation_Violation_Vulnerability); ok { + return x.Vulnerability + } + } + return nil +} + +func (x *PolicyEvaluation_Violation) GetSast() *PolicySASTFinding { + if x != nil { + if x, ok := x.Finding.(*PolicyEvaluation_Violation_Sast); ok { + return x.Sast + } + } + return nil +} + +func (x *PolicyEvaluation_Violation) GetLicenseViolation() *PolicyLicenseViolationFinding { + if x != nil { + if x, ok := x.Finding.(*PolicyEvaluation_Violation_LicenseViolation); ok { + return x.LicenseViolation + } + } + return nil +} + +type isPolicyEvaluation_Violation_Finding interface { + isPolicyEvaluation_Violation_Finding() +} + +type PolicyEvaluation_Violation_Vulnerability struct { + Vulnerability *PolicyVulnerabilityFinding `protobuf:"bytes,3,opt,name=vulnerability,proto3,oneof"` +} + +type PolicyEvaluation_Violation_Sast struct { + Sast *PolicySASTFinding `protobuf:"bytes,4,opt,name=sast,proto3,oneof"` +} + +type PolicyEvaluation_Violation_LicenseViolation struct { + LicenseViolation *PolicyLicenseViolationFinding `protobuf:"bytes,5,opt,name=license_violation,json=licenseViolation,proto3,oneof"` +} + +func (*PolicyEvaluation_Violation_Vulnerability) isPolicyEvaluation_Violation_Finding() {} + +func (*PolicyEvaluation_Violation_Sast) isPolicyEvaluation_Violation_Finding() {} + +func (*PolicyEvaluation_Violation_LicenseViolation) isPolicyEvaluation_Violation_Finding() {} + type PolicyEvaluation_Reference struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -1973,7 +2342,7 @@ type PolicyEvaluation_Reference struct { func (x *PolicyEvaluation_Reference) Reset() { *x = PolicyEvaluation_Reference{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[25] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1985,7 +2354,7 @@ func (x *PolicyEvaluation_Reference) String() string { func (*PolicyEvaluation_Reference) ProtoMessage() {} func (x *PolicyEvaluation_Reference) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[25] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2041,7 +2410,7 @@ type PolicyEvaluation_RawResult struct { func (x *PolicyEvaluation_RawResult) Reset() { *x = PolicyEvaluation_RawResult{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[26] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2053,7 +2422,7 @@ func (x *PolicyEvaluation_RawResult) String() string { func (*PolicyEvaluation_RawResult) ProtoMessage() {} func (x *PolicyEvaluation_RawResult) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[26] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2093,7 +2462,7 @@ type Commit_Remote struct { func (x *Commit_Remote) Reset() { *x = Commit_Remote{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[27] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2105,7 +2474,7 @@ func (x *Commit_Remote) String() string { func (*Commit_Remote) ProtoMessage() {} func (x *Commit_Remote) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[27] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2118,7 +2487,7 @@ func (x *Commit_Remote) ProtoReflect() protoreflect.Message { // Deprecated: Use Commit_Remote.ProtoReflect.Descriptor instead. func (*Commit_Remote) Descriptor() ([]byte, []int) { - return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{4, 0} + return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{7, 0} } func (x *Commit_Remote) GetName() string { @@ -2155,7 +2524,7 @@ type Commit_CommitVerification struct { func (x *Commit_CommitVerification) Reset() { *x = Commit_CommitVerification{} - mi := &file_attestation_v1_crafting_state_proto_msgTypes[28] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2167,7 +2536,7 @@ func (x *Commit_CommitVerification) String() string { func (*Commit_CommitVerification) ProtoMessage() {} func (x *Commit_CommitVerification) ProtoReflect() protoreflect.Message { - mi := &file_attestation_v1_crafting_state_proto_msgTypes[28] + mi := &file_attestation_v1_crafting_state_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2180,7 +2549,7 @@ func (x *Commit_CommitVerification) ProtoReflect() protoreflect.Message { // Deprecated: Use Commit_CommitVerification.ProtoReflect.Descriptor instead. func (*Commit_CommitVerification) Descriptor() ([]byte, []int) { - return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{4, 1} + return file_attestation_v1_crafting_state_proto_rawDescGZIP(), []int{7, 1} } func (x *Commit_CommitVerification) GetAttempted() bool { @@ -2332,7 +2701,7 @@ const file_attestation_v1_crafting_state_proto_rawDesc = "" + "\venvironment\x18\x02 \x01(\tR\venvironment\x12$\n" + "\rauthenticated\x18\x03 \x01(\bR\rauthenticated\x12I\n" + "\x04type\x18\x04 \x01(\x0e25.workflowcontract.v1.CraftingSchema.Runner.RunnerTypeR\x04type\x12\x10\n" + - "\x03url\x18\x05 \x01(\tR\x03url\"\xa1\f\n" + + "\x03url\x18\x05 \x01(\tR\x03url\"\x98\x0e\n" + "\x10PolicyEvaluation\x12\x97\x01\n" + "\x04name\x18\x01 \x01(\tB\x82\x01\xbaH\x7f\xba\x01|\n" + "\rname.dns-1123\x12:must contain only lowercase letters, numbers, and hyphens.\x1a/this.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')R\x04name\x12#\n" + @@ -2362,10 +2731,14 @@ const file_attestation_v1_crafting_state_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a7\n" + "\tWithEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aO\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a\xc5\x02\n" + "\tViolation\x12 \n" + "\asubject\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\asubject\x12 \n" + - "\amessage\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\amessage\x1a\xfc\x01\n" + + "\amessage\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\amessage\x12R\n" + + "\rvulnerability\x18\x03 \x01(\v2*.attestation.v1.PolicyVulnerabilityFindingH\x00R\rvulnerability\x127\n" + + "\x04sast\x18\x04 \x01(\v2!.attestation.v1.PolicySASTFindingH\x00R\x04sast\x12\\\n" + + "\x11license_violation\x18\x05 \x01(\v2-.attestation.v1.PolicyLicenseViolationFindingH\x00R\x10licenseViolationB\t\n" + + "\afinding\x1a\xfc\x01\n" + "\tReference\x12\x97\x01\n" + "\x04name\x18\x01 \x01(\tB\x82\x01\xbaH\x7f\xba\x01|\n" + "\rname.dns-1123\x12:must contain only lowercase letters, numbers, and hyphens.\x1a/this.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')R\x04name\x12\x1f\n" + @@ -2376,7 +2749,34 @@ 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\"\xce\x06\n" + + "\vevaluations\x18\x01 \x03(\v2 .attestation.v1.PolicyEvaluationR\vevaluations\"\xaf\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" + + "externalId\x12)\n" + + "\fpackage_purl\x18\x03 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\vpackagePurl\x12\"\n" + + "\bseverity\x18\x04 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\bseverity\x12;\n" + + "\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\"\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" + + "\bseverity\x18\x03 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\bseverity\x12\"\n" + + "\blocation\x18\x04 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\blocation\x12\x1f\n" + + "\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" + + "\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" + + "\fpackage_purl\x18\x03 \x01(\tR\vpackagePurl\x12%\n" + + "\n" + + "license_id\x18\x04 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\tlicenseId\x12!\n" + + "\flicense_name\x18\x05 \x01(\tR\vlicenseName\x12+\n" + + "\x11component_version\x18\x06 \x01(\tR\x10componentVersion\x12&\n" + + "\x0erecommendation\x18\a \x01(\tR\x0erecommendation\"\xce\x06\n" + "\x06Commit\x12\x1b\n" + "\x04hash\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x04hash\x12!\n" + "\fauthor_email\x18\x02 \x01(\tR\vauthorEmail\x12(\n" + @@ -2457,7 +2857,7 @@ func file_attestation_v1_crafting_state_proto_rawDescGZIP() []byte { } var file_attestation_v1_crafting_state_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_attestation_v1_crafting_state_proto_msgTypes = make([]protoimpl.MessageInfo, 30) +var file_attestation_v1_crafting_state_proto_msgTypes = make([]protoimpl.MessageInfo, 33) var file_attestation_v1_crafting_state_proto_goTypes = []any{ (Attestation_Auth_AuthType)(0), // 0: attestation.v1.Attestation.Auth.AuthType (Commit_CommitVerification_VerificationStatus)(0), // 1: attestation.v1.Commit.CommitVerification.VerificationStatus @@ -2465,90 +2865,96 @@ var file_attestation_v1_crafting_state_proto_goTypes = []any{ (*RunnerEnvironment)(nil), // 3: attestation.v1.RunnerEnvironment (*PolicyEvaluation)(nil), // 4: attestation.v1.PolicyEvaluation (*PolicyEvaluationBundle)(nil), // 5: attestation.v1.PolicyEvaluationBundle - (*Commit)(nil), // 6: attestation.v1.Commit - (*CraftingState)(nil), // 7: attestation.v1.CraftingState - (*WorkflowMetadata)(nil), // 8: attestation.v1.WorkflowMetadata - (*ProjectVersion)(nil), // 9: attestation.v1.ProjectVersion - (*ResourceDescriptor)(nil), // 10: attestation.v1.ResourceDescriptor - nil, // 11: attestation.v1.Attestation.MaterialsEntry - nil, // 12: attestation.v1.Attestation.AnnotationsEntry - (*Attestation_Material)(nil), // 13: attestation.v1.Attestation.Material - nil, // 14: attestation.v1.Attestation.EnvVarsEntry - (*Attestation_Auth)(nil), // 15: attestation.v1.Attestation.Auth - (*Attestation_CASBackend)(nil), // 16: attestation.v1.Attestation.CASBackend - (*Attestation_SigningOptions)(nil), // 17: attestation.v1.Attestation.SigningOptions - nil, // 18: attestation.v1.Attestation.Material.AnnotationsEntry - (*Attestation_Material_KeyVal)(nil), // 19: attestation.v1.Attestation.Material.KeyVal - (*Attestation_Material_ContainerImage)(nil), // 20: attestation.v1.Attestation.Material.ContainerImage - (*Attestation_Material_Artifact)(nil), // 21: attestation.v1.Attestation.Material.Artifact - (*Attestation_Material_SBOMArtifact)(nil), // 22: attestation.v1.Attestation.Material.SBOMArtifact - (*Attestation_Material_SBOMArtifact_MainComponent)(nil), // 23: attestation.v1.Attestation.Material.SBOMArtifact.MainComponent - nil, // 24: attestation.v1.PolicyEvaluation.AnnotationsEntry - nil, // 25: attestation.v1.PolicyEvaluation.WithEntry - (*PolicyEvaluation_Violation)(nil), // 26: attestation.v1.PolicyEvaluation.Violation - (*PolicyEvaluation_Reference)(nil), // 27: attestation.v1.PolicyEvaluation.Reference - (*PolicyEvaluation_RawResult)(nil), // 28: attestation.v1.PolicyEvaluation.RawResult - (*Commit_Remote)(nil), // 29: attestation.v1.Commit.Remote - (*Commit_CommitVerification)(nil), // 30: attestation.v1.Commit.CommitVerification - nil, // 31: attestation.v1.ResourceDescriptor.DigestEntry - (*timestamppb.Timestamp)(nil), // 32: google.protobuf.Timestamp - (v1.CraftingSchema_Runner_RunnerType)(0), // 33: workflowcontract.v1.CraftingSchema.Runner.RunnerType - (v1.CraftingSchema_Material_MaterialType)(0), // 34: workflowcontract.v1.CraftingSchema.Material.MaterialType - (*v1.CraftingSchema)(nil), // 35: workflowcontract.v1.CraftingSchema - (*v1.CraftingSchemaV2)(nil), // 36: workflowcontract.v1.CraftingSchemaV2 - (*structpb.Struct)(nil), // 37: google.protobuf.Struct - (*wrapperspb.BoolValue)(nil), // 38: google.protobuf.BoolValue + (*PolicyVulnerabilityFinding)(nil), // 6: attestation.v1.PolicyVulnerabilityFinding + (*PolicySASTFinding)(nil), // 7: attestation.v1.PolicySASTFinding + (*PolicyLicenseViolationFinding)(nil), // 8: attestation.v1.PolicyLicenseViolationFinding + (*Commit)(nil), // 9: attestation.v1.Commit + (*CraftingState)(nil), // 10: attestation.v1.CraftingState + (*WorkflowMetadata)(nil), // 11: attestation.v1.WorkflowMetadata + (*ProjectVersion)(nil), // 12: attestation.v1.ProjectVersion + (*ResourceDescriptor)(nil), // 13: attestation.v1.ResourceDescriptor + nil, // 14: attestation.v1.Attestation.MaterialsEntry + nil, // 15: attestation.v1.Attestation.AnnotationsEntry + (*Attestation_Material)(nil), // 16: attestation.v1.Attestation.Material + nil, // 17: attestation.v1.Attestation.EnvVarsEntry + (*Attestation_Auth)(nil), // 18: attestation.v1.Attestation.Auth + (*Attestation_CASBackend)(nil), // 19: attestation.v1.Attestation.CASBackend + (*Attestation_SigningOptions)(nil), // 20: attestation.v1.Attestation.SigningOptions + nil, // 21: attestation.v1.Attestation.Material.AnnotationsEntry + (*Attestation_Material_KeyVal)(nil), // 22: attestation.v1.Attestation.Material.KeyVal + (*Attestation_Material_ContainerImage)(nil), // 23: attestation.v1.Attestation.Material.ContainerImage + (*Attestation_Material_Artifact)(nil), // 24: attestation.v1.Attestation.Material.Artifact + (*Attestation_Material_SBOMArtifact)(nil), // 25: attestation.v1.Attestation.Material.SBOMArtifact + (*Attestation_Material_SBOMArtifact_MainComponent)(nil), // 26: attestation.v1.Attestation.Material.SBOMArtifact.MainComponent + nil, // 27: attestation.v1.PolicyEvaluation.AnnotationsEntry + nil, // 28: attestation.v1.PolicyEvaluation.WithEntry + (*PolicyEvaluation_Violation)(nil), // 29: attestation.v1.PolicyEvaluation.Violation + (*PolicyEvaluation_Reference)(nil), // 30: attestation.v1.PolicyEvaluation.Reference + (*PolicyEvaluation_RawResult)(nil), // 31: attestation.v1.PolicyEvaluation.RawResult + (*Commit_Remote)(nil), // 32: attestation.v1.Commit.Remote + (*Commit_CommitVerification)(nil), // 33: attestation.v1.Commit.CommitVerification + nil, // 34: attestation.v1.ResourceDescriptor.DigestEntry + (*timestamppb.Timestamp)(nil), // 35: google.protobuf.Timestamp + (v1.CraftingSchema_Runner_RunnerType)(0), // 36: workflowcontract.v1.CraftingSchema.Runner.RunnerType + (v1.CraftingSchema_Material_MaterialType)(0), // 37: workflowcontract.v1.CraftingSchema.Material.MaterialType + (*v1.CraftingSchema)(nil), // 38: workflowcontract.v1.CraftingSchema + (*v1.CraftingSchemaV2)(nil), // 39: workflowcontract.v1.CraftingSchemaV2 + (*structpb.Struct)(nil), // 40: google.protobuf.Struct + (*wrapperspb.BoolValue)(nil), // 41: google.protobuf.BoolValue } var file_attestation_v1_crafting_state_proto_depIdxs = []int32{ - 32, // 0: attestation.v1.Attestation.initialized_at:type_name -> google.protobuf.Timestamp - 32, // 1: attestation.v1.Attestation.finished_at:type_name -> google.protobuf.Timestamp - 8, // 2: attestation.v1.Attestation.workflow:type_name -> attestation.v1.WorkflowMetadata - 11, // 3: attestation.v1.Attestation.materials:type_name -> attestation.v1.Attestation.MaterialsEntry - 12, // 4: attestation.v1.Attestation.annotations:type_name -> attestation.v1.Attestation.AnnotationsEntry - 14, // 5: attestation.v1.Attestation.env_vars:type_name -> attestation.v1.Attestation.EnvVarsEntry - 33, // 6: attestation.v1.Attestation.runner_type:type_name -> workflowcontract.v1.CraftingSchema.Runner.RunnerType - 6, // 7: attestation.v1.Attestation.head:type_name -> attestation.v1.Commit + 35, // 0: attestation.v1.Attestation.initialized_at:type_name -> google.protobuf.Timestamp + 35, // 1: attestation.v1.Attestation.finished_at:type_name -> google.protobuf.Timestamp + 11, // 2: attestation.v1.Attestation.workflow:type_name -> attestation.v1.WorkflowMetadata + 14, // 3: attestation.v1.Attestation.materials:type_name -> attestation.v1.Attestation.MaterialsEntry + 15, // 4: attestation.v1.Attestation.annotations:type_name -> attestation.v1.Attestation.AnnotationsEntry + 17, // 5: attestation.v1.Attestation.env_vars:type_name -> attestation.v1.Attestation.EnvVarsEntry + 36, // 6: attestation.v1.Attestation.runner_type:type_name -> workflowcontract.v1.CraftingSchema.Runner.RunnerType + 9, // 7: attestation.v1.Attestation.head:type_name -> attestation.v1.Commit 4, // 8: attestation.v1.Attestation.policy_evaluations:type_name -> attestation.v1.PolicyEvaluation - 17, // 9: attestation.v1.Attestation.signing_options:type_name -> attestation.v1.Attestation.SigningOptions + 20, // 9: attestation.v1.Attestation.signing_options:type_name -> attestation.v1.Attestation.SigningOptions 3, // 10: attestation.v1.Attestation.runner_environment:type_name -> attestation.v1.RunnerEnvironment - 15, // 11: attestation.v1.Attestation.auth:type_name -> attestation.v1.Attestation.Auth - 16, // 12: attestation.v1.Attestation.cas_backend:type_name -> attestation.v1.Attestation.CASBackend - 33, // 13: attestation.v1.RunnerEnvironment.type:type_name -> workflowcontract.v1.CraftingSchema.Runner.RunnerType - 24, // 14: attestation.v1.PolicyEvaluation.annotations:type_name -> attestation.v1.PolicyEvaluation.AnnotationsEntry - 26, // 15: attestation.v1.PolicyEvaluation.violations:type_name -> attestation.v1.PolicyEvaluation.Violation - 25, // 16: attestation.v1.PolicyEvaluation.with:type_name -> attestation.v1.PolicyEvaluation.WithEntry - 34, // 17: attestation.v1.PolicyEvaluation.type:type_name -> workflowcontract.v1.CraftingSchema.Material.MaterialType - 27, // 18: attestation.v1.PolicyEvaluation.policy_reference:type_name -> attestation.v1.PolicyEvaluation.Reference - 27, // 19: attestation.v1.PolicyEvaluation.group_reference:type_name -> attestation.v1.PolicyEvaluation.Reference - 28, // 20: attestation.v1.PolicyEvaluation.raw_results:type_name -> attestation.v1.PolicyEvaluation.RawResult + 18, // 11: attestation.v1.Attestation.auth:type_name -> attestation.v1.Attestation.Auth + 19, // 12: attestation.v1.Attestation.cas_backend:type_name -> attestation.v1.Attestation.CASBackend + 36, // 13: attestation.v1.RunnerEnvironment.type:type_name -> workflowcontract.v1.CraftingSchema.Runner.RunnerType + 27, // 14: attestation.v1.PolicyEvaluation.annotations:type_name -> attestation.v1.PolicyEvaluation.AnnotationsEntry + 29, // 15: attestation.v1.PolicyEvaluation.violations:type_name -> attestation.v1.PolicyEvaluation.Violation + 28, // 16: attestation.v1.PolicyEvaluation.with:type_name -> attestation.v1.PolicyEvaluation.WithEntry + 37, // 17: attestation.v1.PolicyEvaluation.type:type_name -> workflowcontract.v1.CraftingSchema.Material.MaterialType + 30, // 18: attestation.v1.PolicyEvaluation.policy_reference:type_name -> attestation.v1.PolicyEvaluation.Reference + 30, // 19: attestation.v1.PolicyEvaluation.group_reference:type_name -> attestation.v1.PolicyEvaluation.Reference + 31, // 20: attestation.v1.PolicyEvaluation.raw_results:type_name -> attestation.v1.PolicyEvaluation.RawResult 4, // 21: attestation.v1.PolicyEvaluationBundle.evaluations:type_name -> attestation.v1.PolicyEvaluation - 32, // 22: attestation.v1.Commit.date:type_name -> google.protobuf.Timestamp - 29, // 23: attestation.v1.Commit.remotes:type_name -> attestation.v1.Commit.Remote - 30, // 24: attestation.v1.Commit.platform_verification:type_name -> attestation.v1.Commit.CommitVerification - 35, // 25: attestation.v1.CraftingState.input_schema:type_name -> workflowcontract.v1.CraftingSchema - 36, // 26: attestation.v1.CraftingState.schema_v2:type_name -> workflowcontract.v1.CraftingSchemaV2 + 35, // 22: attestation.v1.Commit.date:type_name -> google.protobuf.Timestamp + 32, // 23: attestation.v1.Commit.remotes:type_name -> attestation.v1.Commit.Remote + 33, // 24: attestation.v1.Commit.platform_verification:type_name -> attestation.v1.Commit.CommitVerification + 38, // 25: attestation.v1.CraftingState.input_schema:type_name -> workflowcontract.v1.CraftingSchema + 39, // 26: attestation.v1.CraftingState.schema_v2:type_name -> workflowcontract.v1.CraftingSchemaV2 2, // 27: attestation.v1.CraftingState.attestation:type_name -> attestation.v1.Attestation - 9, // 28: attestation.v1.WorkflowMetadata.version:type_name -> attestation.v1.ProjectVersion - 31, // 29: attestation.v1.ResourceDescriptor.digest:type_name -> attestation.v1.ResourceDescriptor.DigestEntry - 37, // 30: attestation.v1.ResourceDescriptor.annotations:type_name -> google.protobuf.Struct - 13, // 31: attestation.v1.Attestation.MaterialsEntry.value:type_name -> attestation.v1.Attestation.Material - 19, // 32: attestation.v1.Attestation.Material.string:type_name -> attestation.v1.Attestation.Material.KeyVal - 20, // 33: attestation.v1.Attestation.Material.container_image:type_name -> attestation.v1.Attestation.Material.ContainerImage - 21, // 34: attestation.v1.Attestation.Material.artifact:type_name -> attestation.v1.Attestation.Material.Artifact - 22, // 35: attestation.v1.Attestation.Material.sbom_artifact:type_name -> attestation.v1.Attestation.Material.SBOMArtifact - 32, // 36: attestation.v1.Attestation.Material.added_at:type_name -> google.protobuf.Timestamp - 34, // 37: attestation.v1.Attestation.Material.material_type:type_name -> workflowcontract.v1.CraftingSchema.Material.MaterialType - 18, // 38: attestation.v1.Attestation.Material.annotations:type_name -> attestation.v1.Attestation.Material.AnnotationsEntry + 12, // 28: attestation.v1.WorkflowMetadata.version:type_name -> attestation.v1.ProjectVersion + 34, // 29: attestation.v1.ResourceDescriptor.digest:type_name -> attestation.v1.ResourceDescriptor.DigestEntry + 40, // 30: attestation.v1.ResourceDescriptor.annotations:type_name -> google.protobuf.Struct + 16, // 31: attestation.v1.Attestation.MaterialsEntry.value:type_name -> attestation.v1.Attestation.Material + 22, // 32: attestation.v1.Attestation.Material.string:type_name -> attestation.v1.Attestation.Material.KeyVal + 23, // 33: attestation.v1.Attestation.Material.container_image:type_name -> attestation.v1.Attestation.Material.ContainerImage + 24, // 34: attestation.v1.Attestation.Material.artifact:type_name -> attestation.v1.Attestation.Material.Artifact + 25, // 35: attestation.v1.Attestation.Material.sbom_artifact:type_name -> attestation.v1.Attestation.Material.SBOMArtifact + 35, // 36: attestation.v1.Attestation.Material.added_at:type_name -> google.protobuf.Timestamp + 37, // 37: attestation.v1.Attestation.Material.material_type:type_name -> workflowcontract.v1.CraftingSchema.Material.MaterialType + 21, // 38: attestation.v1.Attestation.Material.annotations:type_name -> attestation.v1.Attestation.Material.AnnotationsEntry 0, // 39: attestation.v1.Attestation.Auth.type:type_name -> attestation.v1.Attestation.Auth.AuthType - 38, // 40: attestation.v1.Attestation.Material.ContainerImage.has_latest_tag:type_name -> google.protobuf.BoolValue - 21, // 41: attestation.v1.Attestation.Material.SBOMArtifact.artifact:type_name -> attestation.v1.Attestation.Material.Artifact - 23, // 42: attestation.v1.Attestation.Material.SBOMArtifact.main_component:type_name -> attestation.v1.Attestation.Material.SBOMArtifact.MainComponent - 1, // 43: attestation.v1.Commit.CommitVerification.status:type_name -> attestation.v1.Commit.CommitVerification.VerificationStatus - 44, // [44:44] is the sub-list for method output_type - 44, // [44:44] is the sub-list for method input_type - 44, // [44:44] is the sub-list for extension type_name - 44, // [44:44] is the sub-list for extension extendee - 0, // [0:44] is the sub-list for field type_name + 41, // 40: attestation.v1.Attestation.Material.ContainerImage.has_latest_tag:type_name -> google.protobuf.BoolValue + 24, // 41: attestation.v1.Attestation.Material.SBOMArtifact.artifact:type_name -> attestation.v1.Attestation.Material.Artifact + 26, // 42: attestation.v1.Attestation.Material.SBOMArtifact.main_component:type_name -> attestation.v1.Attestation.Material.SBOMArtifact.MainComponent + 6, // 43: attestation.v1.PolicyEvaluation.Violation.vulnerability:type_name -> attestation.v1.PolicyVulnerabilityFinding + 7, // 44: attestation.v1.PolicyEvaluation.Violation.sast:type_name -> attestation.v1.PolicySASTFinding + 8, // 45: attestation.v1.PolicyEvaluation.Violation.license_violation:type_name -> attestation.v1.PolicyLicenseViolationFinding + 1, // 46: attestation.v1.Commit.CommitVerification.status:type_name -> attestation.v1.Commit.CommitVerification.VerificationStatus + 47, // [47:47] is the sub-list for method output_type + 47, // [47:47] is the sub-list for method input_type + 47, // [47:47] is the sub-list for extension type_name + 47, // [47:47] is the sub-list for extension extendee + 0, // [0:47] is the sub-list for field type_name } func init() { file_attestation_v1_crafting_state_proto_init() } @@ -2556,24 +2962,29 @@ func file_attestation_v1_crafting_state_proto_init() { if File_attestation_v1_crafting_state_proto != nil { return } - file_attestation_v1_crafting_state_proto_msgTypes[4].OneofWrappers = []any{} - 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), (*CraftingState_SchemaV2)(nil), } - file_attestation_v1_crafting_state_proto_msgTypes[11].OneofWrappers = []any{ + file_attestation_v1_crafting_state_proto_msgTypes[14].OneofWrappers = []any{ (*Attestation_Material_String_)(nil), (*Attestation_Material_ContainerImage_)(nil), (*Attestation_Material_Artifact_)(nil), (*Attestation_Material_SbomArtifact)(nil), } + file_attestation_v1_crafting_state_proto_msgTypes[27].OneofWrappers = []any{ + (*PolicyEvaluation_Violation_Vulnerability)(nil), + (*PolicyEvaluation_Violation_Sast)(nil), + (*PolicyEvaluation_Violation_LicenseViolation)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_attestation_v1_crafting_state_proto_rawDesc), len(file_attestation_v1_crafting_state_proto_rawDesc)), NumEnums: 2, - NumMessages: 30, + NumMessages: 33, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto b/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto index 4791b270f..9ede42ad1 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto @@ -275,6 +275,15 @@ message PolicyEvaluation { message Violation { string subject = 1 [(buf.validate.field).required = true]; string message = 2 [(buf.validate.field).required = true]; + + // Structured finding data, validated against the policy's declared finding_type. + // Only populated in the CAS-stored PolicyEvaluationBundle, not in the + // inline attestation predicate. + oneof finding { + PolicyVulnerabilityFinding vulnerability = 3; + PolicySASTFinding sast = 4; + PolicyLicenseViolationFinding license_violation = 5; + } } message Reference { @@ -303,6 +312,66 @@ message PolicyEvaluationBundle { repeated PolicyEvaluation evaluations = 1; } +// Output schema for vulnerability findings from policy evaluation. +// Used when a policy declares finding_type: VULNERABILITY. +message PolicyVulnerabilityFinding { + // Human-readable violation description + string message = 1 [(buf.validate.field).required = true]; + // Vulnerability identifier (e.g., CVE-2024-1234, GHSA-xxxx) + string external_id = 2 [(buf.validate.field).required = true]; + // Package URL of the affected component (e.g., pkg:golang/example.com/lib@v1.0.0) + string package_purl = 3 [(buf.validate.field).required = true]; + // Severity level (CRITICAL, HIGH, MEDIUM, LOW) + string severity = 4 [(buf.validate.field).required = true]; + // CVSS v3 score (0.0-10.0) + double cvss_v3_score = 5 [(buf.validate.field).double = { + gte: 0 + lte: 10 + }]; + // CWE references (e.g., CWE-79, CWE-89) + repeated string cwes = 6; + // Suggested fix or upgrade path + string recommendation = 7; +} + +// Output schema for SAST findings from policy evaluation. +// Used when a policy declares finding_type: SAST. +message PolicySASTFinding { + // Human-readable violation description + string message = 1 [(buf.validate.field).required = true]; + // Tool-specific rule identifier (e.g., java:S1234, go-sec:G101) + string rule_id = 2 [(buf.validate.field).required = true]; + // Severity level (CRITICAL, HIGH, MEDIUM, LOW) + string severity = 3 [(buf.validate.field).required = true]; + // File path where the issue was found + string location = 4 [(buf.validate.field).required = true]; + // Line number in the file + int32 line_number = 5; + // Code snippet showing the issue + string code_snippet = 6; + // Suggested fix + string recommendation = 7; +} + +// Output schema for license violation findings from policy evaluation. +// Used when a policy declares finding_type: LICENSE_VIOLATION. +message PolicyLicenseViolationFinding { + // Human-readable violation description + string message = 1 [(buf.validate.field).required = true]; + // Component name (e.g., "lodash") + string component_name = 2 [(buf.validate.field).required = true]; + // Package URL (e.g., pkg:npm/lodash@4.17.21) + string package_purl = 3; + // License identifier (e.g., "GPL-3.0", "AGPL-3.0") + string license_id = 4 [(buf.validate.field).required = true]; + // Human-readable license name + string license_name = 5; + // Component version + string component_version = 6; + // Suggested fix + string recommendation = 7; +} + message Commit { string hash = 1 [(buf.validate.field).string.min_len = 1]; // Commit authors might not include email i.e "Flux <>" diff --git a/pkg/attestation/renderer/chainloop/testdata/attestation.output.v0.2.json b/pkg/attestation/renderer/chainloop/testdata/attestation.output.v0.2.json index c16fa60d0..29a93cba3 100644 --- a/pkg/attestation/renderer/chainloop/testdata/attestation.output.v0.2.json +++ b/pkg/attestation/renderer/chainloop/testdata/attestation.output.v0.2.json @@ -4,7 +4,7 @@ { "name": "chainloop.workflow.skipped", "digest": { - "sha256": "d0c4d6f5a0c8d151588aaf35658040405a44428c2486c01f1f55ef8d29ece97f" + "sha256": "f594b1cf188dfaf2dc1971a1829e07a33582c1a2c6a7a00ffe2abc11064be004" } }, { diff --git a/pkg/attestation/renderer/chainloop/testdata/attestation.output.v2.json b/pkg/attestation/renderer/chainloop/testdata/attestation.output.v2.json index c16fa60d0..29a93cba3 100644 --- a/pkg/attestation/renderer/chainloop/testdata/attestation.output.v2.json +++ b/pkg/attestation/renderer/chainloop/testdata/attestation.output.v2.json @@ -4,7 +4,7 @@ { "name": "chainloop.workflow.skipped", "digest": { - "sha256": "d0c4d6f5a0c8d151588aaf35658040405a44428c2486c01f1f55ef8d29ece97f" + "sha256": "f594b1cf188dfaf2dc1971a1829e07a33582c1a2c6a7a00ffe2abc11064be004" } }, { diff --git a/pkg/policies/engine/engine.go b/pkg/policies/engine/engine.go index 1c32af6af..030f08544 100644 --- a/pkg/policies/engine/engine.go +++ b/pkg/policies/engine/engine.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -157,6 +157,9 @@ type RawData struct { type PolicyViolation struct { Subject string `json:"subject"` Violation string `json:"violation"` + // RawFinding holds the original structured violation object when the policy + // returns a map instead of a string. It is nil for plain-string violations. + RawFinding map[string]any `json:"rawFinding,omitempty"` } // Policy represents a loaded policy in any of the supported engines. @@ -167,6 +170,27 @@ type Policy struct { Name string `json:"name"` } +// NewStructuredViolation creates a PolicyViolation from a structured (map) +// violation object. The object must contain a "message" string field. +// The full object is preserved in RawFinding for downstream validation. +func NewStructuredViolation(policyName string, obj map[string]any) (*PolicyViolation, error) { + msg, ok := obj["message"] + if !ok { + return nil, fmt.Errorf("missing required \"message\" field in structured violation") + } + + msgStr, ok := msg.(string) + if !ok { + return nil, fmt.Errorf("\"message\" field must be a string, got %T", msg) + } + + return &PolicyViolation{ + Subject: policyName, + Violation: msgStr, + RawFinding: obj, + }, nil +} + type ResultFormatError struct { Field string } diff --git a/pkg/policies/engine/rego/builtins/vulnerability.go b/pkg/policies/engine/rego/builtins/vulnerability.go new file mode 100644 index 000000000..af7df659f --- /dev/null +++ b/pkg/policies/engine/rego/builtins/vulnerability.go @@ -0,0 +1,86 @@ +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package builtins + +import ( + "errors" + "fmt" + + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/topdown" + "github.com/open-policy-agent/opa/v1/types" +) + +const vulnerabilityBuiltinName = "chainloop.vulnerability" + +func init() { + if err := registerVulnerabilityBuiltin(); err != nil { + panic(fmt.Sprintf("failed to register vulnerability builtin: %v", err)) + } +} + +// registerVulnerabilityBuiltin registers chainloop.vulnerability as a Rego builtin. +// +// Signature: +// +// chainloop.vulnerability(message, external_id, package_purl, severity) +// +// Returns a structured finding object whose keys match PolicyVulnerabilityFinding +// proto JSON field names. For optional fields (cvss_v3_score, cwes, recommendation), +// use object.union: +// +// v := object.union(chainloop.vulnerability(...), {"cvss_v3_score": 9.8}) +func registerVulnerabilityBuiltin() error { + return Register(&ast.Builtin{ + Name: vulnerabilityBuiltinName, + Description: "Creates a structured vulnerability finding for use in policy violations", + Decl: types.NewFunction( + types.Args( + types.Named("message", types.S).Description("Human-readable violation description"), + types.Named("external_id", types.S).Description("Vulnerability identifier (e.g., CVE-2024-1234)"), + types.Named("package_purl", types.S).Description("Package URL of the affected component"), + types.Named("severity", types.S).Description("Severity level (CRITICAL, HIGH, MEDIUM, LOW)"), + ), + types.Named("finding", types.A).Description("Structured vulnerability finding object"), + ), + }, vulnerabilityImpl) +} + +func vulnerabilityImpl(_ topdown.BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + if _, ok := operands[0].Value.(ast.String); !ok { + return errors.New("message must be a string") + } + + if _, ok := operands[1].Value.(ast.String); !ok { + return errors.New("external_id must be a string") + } + + if _, ok := operands[2].Value.(ast.String); !ok { + return errors.New("package_purl must be a string") + } + + if _, ok := operands[3].Value.(ast.String); !ok { + return errors.New("severity must be a string") + } + + result := ast.NewObject( + ast.Item(ast.StringTerm("message"), operands[0]), + ast.Item(ast.StringTerm("external_id"), operands[1]), + ast.Item(ast.StringTerm("package_purl"), operands[2]), + ast.Item(ast.StringTerm("severity"), operands[3]), + ) + + return iter(ast.NewTerm(result)) +} diff --git a/pkg/policies/engine/rego/builtins/vulnerability_test.go b/pkg/policies/engine/rego/builtins/vulnerability_test.go new file mode 100644 index 000000000..2d7141a5c --- /dev/null +++ b/pkg/policies/engine/rego/builtins/vulnerability_test.go @@ -0,0 +1,138 @@ +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package builtins + +import ( + "context" + "testing" + + "github.com/open-policy-agent/opa/v1/rego" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVulnerabilityBuiltin(t *testing.T) { + tests := []struct { + name string + policy string + expected map[string]any + expectError bool + }{ + { + name: "required fields only", + policy: `package test +import rego.v1 + +result := chainloop.vulnerability("CVE found", "CVE-2024-1234", "pkg:golang/example.com/lib@v1.0.0", "CRITICAL")`, + expected: map[string]any{ + "message": "CVE found", + "external_id": "CVE-2024-1234", + "package_purl": "pkg:golang/example.com/lib@v1.0.0", + "severity": "CRITICAL", + }, + }, + { + name: "combined with object.union for optional fields", + policy: `package test +import rego.v1 + +result := object.union( + chainloop.vulnerability("CVE found", "CVE-2024-1234", "pkg:golang/example.com/lib@v1.0.0", "HIGH"), + {"recommendation": "upgrade to v2.0.0"}, +)`, + expected: map[string]any{ + "message": "CVE found", + "external_id": "CVE-2024-1234", + "package_purl": "pkg:golang/example.com/lib@v1.0.0", + "severity": "HIGH", + "recommendation": "upgrade to v2.0.0", + }, + }, + { + name: "with sprintf interpolation", + policy: `package test +import rego.v1 + +result := chainloop.vulnerability( + sprintf("Found %s", ["CVE-2024-1234"]), + "CVE-2024-1234", + "pkg:npm/foo@1.0", + "MEDIUM", +)`, + expected: map[string]any{ + "message": "Found CVE-2024-1234", + "external_id": "CVE-2024-1234", + "package_purl": "pkg:npm/foo@1.0", + "severity": "MEDIUM", + }, + }, + { + name: "message is not a string", + policy: `package test +import rego.v1 + +result := chainloop.vulnerability(123, "CVE-2024-1234", "pkg:foo", "HIGH")`, + expectError: true, + }, + { + name: "external_id is not a string", + policy: `package test +import rego.v1 + +result := chainloop.vulnerability("msg", 123, "pkg:foo", "HIGH")`, + expectError: true, + }, + { + name: "wrong number of arguments", + policy: `package test +import rego.v1 + +result := chainloop.vulnerability("msg", "CVE-2024-1234", "pkg:foo")`, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + r := rego.New( + rego.Query("data.test.result"), + rego.Module("test.rego", tt.policy), + ) + rs, err := r.Eval(ctx) + + if tt.expectError { + assert.Error(t, err) + return + } + + require.NoError(t, err) + require.Len(t, rs, 1) + require.Len(t, rs[0].Expressions, 1) + + result, ok := rs[0].Expressions[0].Value.(map[string]any) + require.True(t, ok) + + for k, v := range tt.expected { + assert.Equal(t, v, result[k], "field %q mismatch", k) + } + // Ensure no extra fields beyond expected + for k := range result { + _, inExpected := tt.expected[k] + assert.True(t, inExpected, "unexpected field %q in result", k) + } + }) + } +} diff --git a/pkg/policies/engine/rego/rego.go b/pkg/policies/engine/rego/rego.go index cd75ce9a1..796e10a58 100644 --- a/pkg/policies/engine/rego/rego.go +++ b/pkg/policies/engine/rego/rego.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -216,11 +216,18 @@ func parseResultRule(res rego.ResultSet, policy *engine.Policy, rawData *engine. result.Ignore = ignore for _, violation := range violations { - vs, ok := violation.(string) - if !ok { - return nil, fmt.Errorf("failed to evaluate violation in policy evaluation result: %s", val.Text) + switch v := violation.(type) { + case string: + result.Violations = append(result.Violations, &engine.PolicyViolation{Subject: policy.Name, Violation: v}) + case map[string]any: + pv, err := engine.NewStructuredViolation(policy.Name, v) + if err != nil { + return nil, fmt.Errorf("structured violation in policy %q: %w", policy.Name, err) + } + result.Violations = append(result.Violations, pv) + default: + return nil, fmt.Errorf("violation must be a string or object, got %T", violation) } - result.Violations = append(result.Violations, &engine.PolicyViolation{Subject: policy.Name, Violation: vs}) } } } diff --git a/pkg/policies/engine/rego/rego_test.go b/pkg/policies/engine/rego/rego_test.go index c92025ac3..93b038ce4 100644 --- a/pkg/policies/engine/rego/rego_test.go +++ b/pkg/policies/engine/rego/rego_test.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -533,3 +533,224 @@ violations contains msg if { assert.Len(t, result.Violations, 0) }) } + +func TestRego_StructuredViolations(t *testing.T) { + tests := []struct { + name string + fixture string + input string + wantViolations int + wantErr string + checkFn func(t *testing.T, result *engine.EvaluationResult) + }{ + { + name: "structured violations with message field", + fixture: "testfiles/structured_violations.rego", + input: `{"vulnerabilities": [{"id": "CVE-2024-1234", "severity": "CRITICAL", "purl": "pkg:golang/example.com/lib@v1.0.0"}]}`, + wantViolations: 1, + checkFn: func(t *testing.T, result *engine.EvaluationResult) { + t.Helper() + v := result.Violations[0] + assert.Equal(t, "structured-policy", v.Subject) + assert.Equal(t, "Found vulnerability CVE-2024-1234 (CRITICAL)", v.Violation) + assert.NotNil(t, v.RawFinding) + assert.Equal(t, "CVE-2024-1234", v.RawFinding["external_id"]) + assert.Equal(t, "CRITICAL", v.RawFinding["severity"]) + assert.Equal(t, "pkg:golang/example.com/lib@v1.0.0", v.RawFinding["package_purl"]) + }, + }, + { + name: "structured violations without message field errors", + fixture: "testfiles/structured_no_message.rego", + input: `{"vulnerabilities": [{"id": "CVE-2024-1234", "severity": "HIGH"}]}`, + wantErr: `missing required "message" field`, + }, + { + name: "mixed string and structured violations", + fixture: "testfiles/mixed_violations.rego", + input: `{"has_string_violation": true, "vulnerabilities": [{"id": "CVE-2024-5678", "severity": "HIGH"}]}`, + wantViolations: 2, + checkFn: func(t *testing.T, result *engine.EvaluationResult) { + t.Helper() + var stringViolation, structuredViolation *engine.PolicyViolation + for _, v := range result.Violations { + if v.RawFinding == nil { + stringViolation = v + } else { + structuredViolation = v + } + } + require.NotNil(t, stringViolation, "expected a string violation") + assert.Equal(t, "simple string violation", stringViolation.Violation) + assert.Nil(t, stringViolation.RawFinding) + + require.NotNil(t, structuredViolation, "expected a structured violation") + assert.Equal(t, "Found vulnerability CVE-2024-5678", structuredViolation.Violation) + assert.Equal(t, "CVE-2024-5678", structuredViolation.RawFinding["external_id"]) + }, + }, + { + name: "no violations with structured policy", + fixture: "testfiles/structured_violations.rego", + input: `{"vulnerabilities": []}`, + wantViolations: 0, + }, + { + name: "multiple structured violations", + fixture: "testfiles/structured_violations.rego", + input: `{"vulnerabilities": [{"id": "CVE-2024-1", "severity": "HIGH", "purl": "pkg:npm/foo@1.0"}, {"id": "CVE-2024-2", "severity": "LOW", "purl": "pkg:npm/bar@2.0"}]}`, + wantViolations: 2, + checkFn: func(t *testing.T, result *engine.EvaluationResult) { + t.Helper() + ids := make(map[string]bool) + for _, v := range result.Violations { + require.NotNil(t, v.RawFinding) + ids[v.RawFinding["external_id"].(string)] = true + } + assert.True(t, ids["CVE-2024-1"]) + assert.True(t, ids["CVE-2024-2"]) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + regoContent, err := os.ReadFile(tc.fixture) + require.NoError(t, err) + + r := NewEngine() + policy := &engine.Policy{ + Name: "structured-policy", + Source: regoContent, + } + + result, err := r.Verify(context.TODO(), policy, []byte(tc.input), nil) + if tc.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + return + } + + require.NoError(t, err) + assert.Len(t, result.Violations, tc.wantViolations) + if tc.checkFn != nil { + tc.checkFn(t, result) + } + }) + } +} + +func TestRego_VulnerabilityBuiltinIntegration(t *testing.T) { + regoContent, err := os.ReadFile("testfiles/structured_vulnerability_builtin.rego") + require.NoError(t, err) + + r := NewEngine() + policy := &engine.Policy{ + Name: "vuln-builtin-policy", + Source: regoContent, + } + + input := `{ + "vulnerabilities": [ + {"id": "CVE-2024-1234", "severity": "CRITICAL", "purl": "pkg:golang/example.com/lib@v1.0.0"}, + {"id": "CVE-2024-5678", "severity": "HIGH", "purl": "pkg:npm/foo@2.0.0"} + ] + }` + + result, err := r.Verify(context.TODO(), policy, []byte(input), nil) + require.NoError(t, err) + require.Len(t, result.Violations, 2) + + ids := make(map[string]*engine.PolicyViolation) + for _, v := range result.Violations { + require.NotNil(t, v.RawFinding) + ids[v.RawFinding["external_id"].(string)] = v + } + + v1 := ids["CVE-2024-1234"] + require.NotNil(t, v1) + assert.Equal(t, "Found vulnerability CVE-2024-1234 (CRITICAL)", v1.Violation) + assert.Equal(t, "CRITICAL", v1.RawFinding["severity"]) + assert.Equal(t, "pkg:golang/example.com/lib@v1.0.0", v1.RawFinding["package_purl"]) + + v2 := ids["CVE-2024-5678"] + require.NotNil(t, v2) + assert.Equal(t, "Found vulnerability CVE-2024-5678 (HIGH)", v2.Violation) + assert.Equal(t, "HIGH", v2.RawFinding["severity"]) + assert.Equal(t, "pkg:npm/foo@2.0.0", v2.RawFinding["package_purl"]) +} + +func TestRego_StructuredSASTViolations(t *testing.T) { + regoContent, err := os.ReadFile("testfiles/structured_sast.rego") + require.NoError(t, err) + + r := NewEngine() + policy := &engine.Policy{ + Name: "sast-policy", + Source: regoContent, + } + + input := `{ + "findings": [ + { + "rule_id": "java:S1234", + "severity": "HIGH", + "location": "src/main/Handler.java", + "line_number": 42, + "code_snippet": "String query = \"SELECT * FROM users WHERE id=\" + userId;" + } + ] + }` + + result, err := r.Verify(context.TODO(), policy, []byte(input), nil) + require.NoError(t, err) + require.Len(t, result.Violations, 1) + + v := result.Violations[0] + assert.Contains(t, v.Violation, "java:S1234") + assert.NotNil(t, v.RawFinding) + assert.Equal(t, "java:S1234", v.RawFinding["rule_id"]) + assert.Equal(t, "HIGH", v.RawFinding["severity"]) + assert.Equal(t, "src/main/Handler.java", v.RawFinding["location"]) +} + +func TestRego_StructuredLicenseViolations(t *testing.T) { + regoContent, err := os.ReadFile("testfiles/structured_license.rego") + require.NoError(t, err) + + r := NewEngine() + policy := &engine.Policy{ + Name: "license-policy", + Source: regoContent, + } + + input := `{ + "banned_licenses": ["GPL-3.0", "AGPL-3.0"], + "components": [ + { + "name": "libfoo", + "purl": "pkg:npm/libfoo@2.1.0", + "version": "2.1.0", + "licenses": ["GPL-3.0"] + }, + { + "name": "libbar", + "purl": "pkg:npm/libbar@1.0.0", + "version": "1.0.0", + "licenses": ["MIT"] + } + ] + }` + + result, err := r.Verify(context.TODO(), policy, []byte(input), nil) + require.NoError(t, err) + require.Len(t, result.Violations, 1) + + v := result.Violations[0] + assert.Contains(t, v.Violation, "GPL-3.0") + assert.Contains(t, v.Violation, "libfoo") + assert.NotNil(t, v.RawFinding) + assert.Equal(t, "libfoo", v.RawFinding["component_name"]) + assert.Equal(t, "GPL-3.0", v.RawFinding["license_id"]) + assert.Equal(t, "pkg:npm/libfoo@2.1.0", v.RawFinding["package_purl"]) +} diff --git a/pkg/policies/engine/rego/testfiles/mixed_violations.rego b/pkg/policies/engine/rego/testfiles/mixed_violations.rego new file mode 100644 index 000000000..813c2a120 --- /dev/null +++ b/pkg/policies/engine/rego/testfiles/mixed_violations.rego @@ -0,0 +1,45 @@ +package main + +import rego.v1 + +################################ +# Common section do NOT change # +################################ + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "invalid input" +} + +default skipped := true + +skipped := false if valid_input + +######################################## +# EO Common section, custom code below # +######################################## + +valid_input := true + +# Returns a mix of string and structured violations +violations contains msg if { + input.has_string_violation + msg := "simple string violation" +} + +violations contains v if { + some vuln in input.vulnerabilities + v := { + "message": sprintf("Found vulnerability %s", [vuln.id]), + "external_id": vuln.id, + "severity": vuln.severity, + } +} diff --git a/pkg/policies/engine/rego/testfiles/structured_license.rego b/pkg/policies/engine/rego/testfiles/structured_license.rego new file mode 100644 index 000000000..2a2f010ef --- /dev/null +++ b/pkg/policies/engine/rego/testfiles/structured_license.rego @@ -0,0 +1,44 @@ +package main + +import rego.v1 + +################################ +# Common section do NOT change # +################################ + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "invalid input" +} + +default skipped := true + +skipped := false if valid_input + +######################################## +# EO Common section, custom code below # +######################################## + +valid_input := true + +# Returns structured license violation objects +violations contains v if { + some comp in input.components + some license in comp.licenses + license in input.banned_licenses + v := { + "message": sprintf("Banned license %s found in component %s", [license, comp.name]), + "component_name": comp.name, + "package_purl": comp.purl, + "license_id": license, + "component_version": comp.version, + } +} diff --git a/pkg/policies/engine/rego/testfiles/structured_no_message.rego b/pkg/policies/engine/rego/testfiles/structured_no_message.rego new file mode 100644 index 000000000..a4a958150 --- /dev/null +++ b/pkg/policies/engine/rego/testfiles/structured_no_message.rego @@ -0,0 +1,39 @@ +package main + +import rego.v1 + +################################ +# Common section do NOT change # +################################ + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "invalid input" +} + +default skipped := true + +skipped := false if valid_input + +######################################## +# EO Common section, custom code below # +######################################## + +valid_input := true + +# Returns structured violation objects WITHOUT a "message" field +violations contains v if { + some vuln in input.vulnerabilities + v := { + "external_id": vuln.id, + "severity": vuln.severity, + } +} diff --git a/pkg/policies/engine/rego/testfiles/structured_sast.rego b/pkg/policies/engine/rego/testfiles/structured_sast.rego new file mode 100644 index 000000000..e14d28f29 --- /dev/null +++ b/pkg/policies/engine/rego/testfiles/structured_sast.rego @@ -0,0 +1,43 @@ +package main + +import rego.v1 + +################################ +# Common section do NOT change # +################################ + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "invalid input" +} + +default skipped := true + +skipped := false if valid_input + +######################################## +# EO Common section, custom code below # +######################################## + +valid_input := true + +# Returns structured SAST violation objects +violations contains v if { + some finding in input.findings + v := { + "message": sprintf("SAST finding %s in %s", [finding.rule_id, finding.location]), + "rule_id": finding.rule_id, + "severity": finding.severity, + "location": finding.location, + "line_number": finding.line_number, + "code_snippet": finding.code_snippet, + } +} diff --git a/pkg/policies/engine/rego/testfiles/structured_violations.rego b/pkg/policies/engine/rego/testfiles/structured_violations.rego new file mode 100644 index 000000000..f39c43839 --- /dev/null +++ b/pkg/policies/engine/rego/testfiles/structured_violations.rego @@ -0,0 +1,41 @@ +package main + +import rego.v1 + +################################ +# Common section do NOT change # +################################ + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "invalid input" +} + +default skipped := true + +skipped := false if valid_input + +######################################## +# EO Common section, custom code below # +######################################## + +valid_input := true + +# Returns structured violation objects with a required "message" field +violations contains v if { + some vuln in input.vulnerabilities + v := { + "message": sprintf("Found vulnerability %s (%s)", [vuln.id, vuln.severity]), + "external_id": vuln.id, + "severity": vuln.severity, + "package_purl": vuln.purl, + } +} diff --git a/pkg/policies/engine/rego/testfiles/structured_vulnerability_builtin.rego b/pkg/policies/engine/rego/testfiles/structured_vulnerability_builtin.rego new file mode 100644 index 000000000..735c6890f --- /dev/null +++ b/pkg/policies/engine/rego/testfiles/structured_vulnerability_builtin.rego @@ -0,0 +1,41 @@ +package main + +import rego.v1 + +################################ +# Common section do NOT change # +################################ + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "invalid input" +} + +default skipped := true + +skipped := false if valid_input + +######################################## +# EO Common section, custom code below # +######################################## + +valid_input := true + +# Returns structured vulnerability findings using the chainloop.vulnerability builtin +violations contains v if { + some vuln in input.vulnerabilities + v := chainloop.vulnerability( + sprintf("Found vulnerability %s (%s)", [vuln.id, vuln.severity]), + vuln.id, + vuln.purl, + vuln.severity, + ) +} diff --git a/pkg/policies/engine/wasm/engine.go b/pkg/policies/engine/wasm/engine.go index dc5f2b2fc..e37b5160d 100644 --- a/pkg/policies/engine/wasm/engine.go +++ b/pkg/policies/engine/wasm/engine.go @@ -1,5 +1,5 @@ // -// Copyright 2025 The Chainloop Authors. +// Copyright 2025-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -189,10 +189,10 @@ func (e *Engine) Verify(ctx context.Context, policy *engine.Policy, input []byte // Parse output var result struct { - Skipped bool `json:"skipped"` - Violations []string `json:"violations"` - SkipReason string `json:"skip_reason"` - Ignore bool `json:"ignore"` + Skipped bool `json:"skipped"` + Violations []json.RawMessage `json:"violations"` + SkipReason string `json:"skip_reason"` + Ignore bool `json:"ignore"` } if err := json.Unmarshal(output, &result); err != nil { @@ -208,11 +208,12 @@ func (e *Engine) Verify(ctx context.Context, policy *engine.Policy, input []byte Violations: make([]*engine.PolicyViolation, 0, len(result.Violations)), } - for _, v := range result.Violations { - evalResult.Violations = append(evalResult.Violations, &engine.PolicyViolation{ - Subject: policy.Name, - Violation: v, - }) + for _, raw := range result.Violations { + pv, err := parseWasmViolation(policy.Name, raw) + if err != nil { + return nil, fmt.Errorf("violation in policy %q: %w", policy.Name, err) + } + evalResult.Violations = append(evalResult.Violations, pv) } e.logger.Debug().Str("policy", policy.Name).Int("violations", len(evalResult.Violations)).Bool("skipped", evalResult.Skipped).Msg("WASM policy evaluation complete") @@ -228,6 +229,27 @@ func (e *Engine) Verify(ctx context.Context, policy *engine.Policy, input []byte return evalResult, nil } +// parseWasmViolation parses a single violation from WASM output. +// The violation can be either a JSON string or a JSON object with a required "message" field. +func parseWasmViolation(policyName string, raw json.RawMessage) (*engine.PolicyViolation, error) { + var v any + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("invalid violation JSON: %w", err) + } + + switch typed := v.(type) { + case string: + return &engine.PolicyViolation{ + Subject: policyName, + Violation: typed, + }, nil + case map[string]any: + return engine.NewStructuredViolation(policyName, typed) + default: + return nil, fmt.Errorf("violation must be a string or object, got %T", v) + } +} + // MatchesParameters is a stub implementation for WASM policies // WASM policies don't currently support parameter matching func (e *Engine) MatchesParameters(_ context.Context, _ *engine.Policy, diff --git a/pkg/policies/findings/registry.go b/pkg/policies/findings/registry.go new file mode 100644 index 000000000..1ee67d71a --- /dev/null +++ b/pkg/policies/findings/registry.go @@ -0,0 +1,101 @@ +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package findings + +import ( + "encoding/json" + "fmt" + + v1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" + + "buf.build/go/protovalidate" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +const ( + FindingTypeVulnerability = "VULNERABILITY" + FindingTypeSAST = "SAST" + FindingTypeLicenseViolation = "LICENSE_VIOLATION" +) + +// findingTypes maps declared finding type strings to proto message constructors. +var findingTypes = map[string]func() proto.Message{ + FindingTypeVulnerability: func() proto.Message { return &v1.PolicyVulnerabilityFinding{} }, + FindingTypeSAST: func() proto.Message { return &v1.PolicySASTFinding{} }, + FindingTypeLicenseViolation: func() proto.Message { return &v1.PolicyLicenseViolationFinding{} }, +} + +// IsValidFindingType checks whether a finding type string is recognized. +func IsValidFindingType(findingType string) bool { + _, ok := findingTypes[findingType] + return ok +} + +// ValidateFinding validates a raw violation object against the proto schema +// for the given finding type. It marshals the raw map to JSON, unmarshals into +// the corresponding proto message, and runs buf.validate constraints. +// Returns the validated proto message on success. +func ValidateFinding(findingType string, raw map[string]any) (proto.Message, error) { + factory, ok := findingTypes[findingType] + if !ok { + return nil, fmt.Errorf("unknown finding type %q", findingType) + } + + data, err := json.Marshal(raw) + if err != nil { + return nil, fmt.Errorf("marshaling raw finding to JSON: %w", err) + } + + msg := factory() + if err := protojson.Unmarshal(data, msg); err != nil { + return nil, fmt.Errorf("finding does not match %s schema: %w", findingType, err) + } + + if err := protovalidate.Validate(msg); err != nil { + return nil, fmt.Errorf("finding validation failed for %s: %w", findingType, err) + } + + return msg, nil +} + +// SetViolationFinding populates the oneof finding field on a Violation proto +// based on the finding type and the validated proto message. +func SetViolationFinding(violation *v1.PolicyEvaluation_Violation, findingType string, finding proto.Message) error { + switch findingType { + case FindingTypeVulnerability: + f, ok := finding.(*v1.PolicyVulnerabilityFinding) + if !ok { + return fmt.Errorf("finding is not a PolicyVulnerabilityFinding") + } + violation.Finding = &v1.PolicyEvaluation_Violation_Vulnerability{Vulnerability: f} + case FindingTypeSAST: + f, ok := finding.(*v1.PolicySASTFinding) + if !ok { + return fmt.Errorf("finding is not a PolicySASTFinding") + } + violation.Finding = &v1.PolicyEvaluation_Violation_Sast{Sast: f} + case FindingTypeLicenseViolation: + f, ok := finding.(*v1.PolicyLicenseViolationFinding) + if !ok { + return fmt.Errorf("finding is not a PolicyLicenseViolationFinding") + } + violation.Finding = &v1.PolicyEvaluation_Violation_LicenseViolation{LicenseViolation: f} + default: + return fmt.Errorf("unknown finding type %q", findingType) + } + + return nil +} diff --git a/pkg/policies/findings/registry_test.go b/pkg/policies/findings/registry_test.go new file mode 100644 index 000000000..d46e6c6fe --- /dev/null +++ b/pkg/policies/findings/registry_test.go @@ -0,0 +1,265 @@ +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package findings + +import ( + "testing" + + v1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestIsValidFindingType(t *testing.T) { + tests := []struct { + name string + findingType string + want bool + }{ + {"vulnerability", "VULNERABILITY", true}, + {"sast", "SAST", true}, + {"license_violation", "LICENSE_VIOLATION", true}, + {"unknown", "UNKNOWN", false}, + {"empty", "", false}, + {"lowercase", "vulnerability", false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, IsValidFindingType(tc.findingType)) + }) + } +} + +func TestValidateFinding(t *testing.T) { + tests := []struct { + name string + findingType string + raw map[string]any + wantErr string + checkFn func(t *testing.T, msg interface{}) + }{ + { + name: "valid vulnerability finding", + findingType: "VULNERABILITY", + raw: map[string]any{ + "message": "Found CVE-2024-1234", + "external_id": "CVE-2024-1234", + "package_purl": "pkg:golang/example.com/lib@v1.0.0", + "severity": "CRITICAL", + "cvss_v3_score": 9.8, + }, + checkFn: func(t *testing.T, msg interface{}) { + t.Helper() + f, ok := msg.(*v1.PolicyVulnerabilityFinding) + require.True(t, ok) + assert.Equal(t, "Found CVE-2024-1234", f.GetMessage()) + assert.Equal(t, "CVE-2024-1234", f.GetExternalId()) + assert.Equal(t, "pkg:golang/example.com/lib@v1.0.0", f.GetPackagePurl()) + assert.Equal(t, "CRITICAL", f.GetSeverity()) + assert.InDelta(t, 9.8, f.GetCvssV3Score(), 0.01) + }, + }, + { + name: "vulnerability finding missing required field", + findingType: "VULNERABILITY", + raw: map[string]any{ + "message": "Found CVE-2024-1234", + "external_id": "CVE-2024-1234", + // missing package_purl and severity + }, + wantErr: "finding validation failed", + }, + { + name: "vulnerability finding with out-of-range CVSS score", + findingType: "VULNERABILITY", + raw: map[string]any{ + "message": "Found CVE-2024-1234", + "external_id": "CVE-2024-1234", + "package_purl": "pkg:golang/example.com/lib@v1.0.0", + "severity": "CRITICAL", + "cvss_v3_score": 15.0, + }, + wantErr: "finding validation failed", + }, + { + name: "valid SAST finding", + findingType: "SAST", + raw: map[string]any{ + "message": "SQL injection in handler", + "rule_id": "java:S1234", + "severity": "HIGH", + "location": "src/main/Handler.java", + }, + checkFn: func(t *testing.T, msg interface{}) { + t.Helper() + f, ok := msg.(*v1.PolicySASTFinding) + require.True(t, ok) + assert.Equal(t, "SQL injection in handler", f.GetMessage()) + assert.Equal(t, "java:S1234", f.GetRuleId()) + assert.Equal(t, "HIGH", f.GetSeverity()) + assert.Equal(t, "src/main/Handler.java", f.GetLocation()) + }, + }, + { + name: "SAST finding missing required field", + findingType: "SAST", + raw: map[string]any{ + "message": "SQL injection", + // missing rule_id, severity, location + }, + wantErr: "finding validation failed", + }, + { + name: "valid license violation finding", + findingType: "LICENSE_VIOLATION", + raw: map[string]any{ + "message": "Banned license GPL-3.0", + "component_name": "lodash", + "license_id": "GPL-3.0", + "package_purl": "pkg:npm/lodash@4.17.21", + }, + checkFn: func(t *testing.T, msg interface{}) { + t.Helper() + f, ok := msg.(*v1.PolicyLicenseViolationFinding) + require.True(t, ok) + assert.Equal(t, "Banned license GPL-3.0", f.GetMessage()) + assert.Equal(t, "lodash", f.GetComponentName()) + assert.Equal(t, "GPL-3.0", f.GetLicenseId()) + }, + }, + { + name: "unknown finding type", + findingType: "UNKNOWN", + raw: map[string]any{"message": "test"}, + wantErr: "unknown finding type", + }, + { + name: "invalid field type in raw data", + findingType: "VULNERABILITY", + raw: map[string]any{ + "message": 123, // should be string + "external_id": "CVE-2024-1234", + "package_purl": "pkg:golang/example.com/lib@v1.0.0", + "severity": "HIGH", + }, + wantErr: "does not match VULNERABILITY schema", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + msg, err := ValidateFinding(tc.findingType, tc.raw) + if tc.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + return + } + + require.NoError(t, err) + require.NotNil(t, msg) + if tc.checkFn != nil { + tc.checkFn(t, msg) + } + }) + } +} + +func TestSetViolationFinding(t *testing.T) { + tests := []struct { + name string + findingType string + finding proto.Message + wantErr string + checkFn func(t *testing.T, v *v1.PolicyEvaluation_Violation) + }{ + { + name: "set vulnerability finding", + findingType: "VULNERABILITY", + finding: &v1.PolicyVulnerabilityFinding{ + Message: "test", + ExternalId: "CVE-2024-1", + PackagePurl: "pkg:npm/foo@1.0", + Severity: "HIGH", + }, + checkFn: func(t *testing.T, v *v1.PolicyEvaluation_Violation) { + t.Helper() + f := v.GetVulnerability() + require.NotNil(t, f) + assert.Equal(t, "CVE-2024-1", f.GetExternalId()) + }, + }, + { + name: "set SAST finding", + findingType: "SAST", + finding: &v1.PolicySASTFinding{ + Message: "test", + RuleId: "go-sec:G101", + Severity: "MEDIUM", + Location: "main.go", + }, + 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()) + }, + }, + { + name: "set license violation finding", + findingType: "LICENSE_VIOLATION", + finding: &v1.PolicyLicenseViolationFinding{ + Message: "test", + ComponentName: "foo", + LicenseId: "MIT", + }, + checkFn: func(t *testing.T, v *v1.PolicyEvaluation_Violation) { + t.Helper() + f := v.GetLicenseViolation() + require.NotNil(t, f) + assert.Equal(t, "foo", f.GetComponentName()) + }, + }, + { + name: "unknown finding type", + findingType: "UNKNOWN", + finding: &v1.PolicyVulnerabilityFinding{}, + wantErr: "unknown finding type", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + violation := &v1.PolicyEvaluation_Violation{ + Subject: "test-policy", + Message: "test violation", + } + + err := SetViolationFinding(violation, tc.findingType, tc.finding) + if tc.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + return + } + + require.NoError(t, err) + if tc.checkFn != nil { + tc.checkFn(t, violation) + } + }) + } +} diff --git a/pkg/policies/policies.go b/pkg/policies/policies.go index f34e182b9..e67aca313 100644 --- a/pkg/policies/policies.go +++ b/pkg/policies/policies.go @@ -44,6 +44,7 @@ import ( "github.com/chainloop-dev/chainloop/pkg/policies/engine" "github.com/chainloop-dev/chainloop/pkg/policies/engine/rego" "github.com/chainloop-dev/chainloop/pkg/policies/engine/wasm" + "github.com/chainloop-dev/chainloop/pkg/policies/findings" "golang.org/x/sync/errgroup" ) @@ -387,13 +388,23 @@ func (pv *PolicyVerifier) evaluatePolicyAttachment(ctx context.Context, attachme reasons = []string{} } + findingType := policy.GetMetadata().GetFindingType() + apiViolations, warnings, err := engineEvaluationsToAPIViolations(evalResults, findingType) + if err != nil { + return nil, NewPolicyError(fmt.Errorf("policy %q: %w", policy.GetMetadata().GetName(), err)) + } + + for _, w := range warnings { + pv.logger.Warn().Str("policy", policy.GetMetadata().GetName()).Msg(w) + } + // Merge multi-kind results return &v12.PolicyEvaluation{ Name: policy.GetMetadata().GetName(), MaterialName: opts.name, Sources: evaluationSources, // merge all violations - Violations: engineEvaluationsToAPIViolations(evalResults), + Violations: apiViolations, Annotations: policy.GetMetadata().GetAnnotations(), Description: policy.GetMetadata().GetDescription(), With: args, @@ -743,18 +754,50 @@ func splitArgs(s string) []string { return result } -func engineEvaluationsToAPIViolations(results []*engine.EvaluationResult) []*v12.PolicyEvaluation_Violation { +func engineEvaluationsToAPIViolations(results []*engine.EvaluationResult, findingType string) ([]*v12.PolicyEvaluation_Violation, []string, error) { res := make([]*v12.PolicyEvaluation_Violation, 0) + var warnings []string + warnedNoFindingType := false + for _, r := range results { for _, v := range r.Violations { - res = append(res, &v12.PolicyEvaluation_Violation{ + apiV := &v12.PolicyEvaluation_Violation{ Subject: v.Subject, Message: v.Violation, - }) + } + + hasStructuredData := v.RawFinding != nil + + switch { + case findingType == "" && !hasStructuredData: + // No finding_type, string violation — current behavior + + case findingType == "" && hasStructuredData: + if !warnedNoFindingType { + warnings = append(warnings, + "policy returns structured violation objects but does not declare finding_type in metadata — finding data will be ignored") + warnedNoFindingType = true + } + + case findingType != "" && !hasStructuredData: + return nil, nil, fmt.Errorf("declares finding_type %q but violation is not a structured object", findingType) + + case findingType != "" && hasStructuredData: + finding, err := findings.ValidateFinding(findingType, v.RawFinding) + if err != nil { + return nil, nil, fmt.Errorf("structured violation validation: %w", err) + } + + if err := findings.SetViolationFinding(apiV, findingType, finding); err != nil { + return nil, nil, fmt.Errorf("setting violation finding: %w", err) + } + } + + res = append(res, apiV) } } - return res + return res, warnings, nil } // returns the list of polices to be applied to a material, following these rules: diff --git a/pkg/policies/policies_test.go b/pkg/policies/policies_test.go index d0bef1a46..9e4d07471 100644 --- a/pkg/policies/policies_test.go +++ b/pkg/policies/policies_test.go @@ -23,6 +23,7 @@ import ( v12 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" v1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" + "github.com/chainloop-dev/chainloop/pkg/policies/engine" intoto "github.com/in-toto/attestation/go/v1" "github.com/rs/zerolog" "github.com/stretchr/testify/suite" @@ -1488,3 +1489,131 @@ func (s *testSuite) TestPolicyAttachmentGate() { }) } } + +func (s *testSuite) TestEngineEvaluationsToAPIViolationsBehaviorMatrix() { + cases := []struct { + name string + findingType string + violations []*engine.PolicyViolation + wantErr string + wantWarnings int + wantViolations int + checkFn func(violations []*v1.PolicyEvaluation_Violation) + }{ + { + name: "no finding_type + string violations - current behavior", + findingType: "", + violations: []*engine.PolicyViolation{ + {Subject: "p1", Violation: "something failed"}, + }, + wantViolations: 1, + }, + { + name: "no finding_type + structured violations - warning emitted", + findingType: "", + violations: []*engine.PolicyViolation{ + {Subject: "p1", Violation: "vuln found", RawFinding: map[string]any{ + "message": "vuln found", "external_id": "CVE-1", + }}, + }, + wantViolations: 1, + wantWarnings: 1, + checkFn: func(violations []*v1.PolicyEvaluation_Violation) { + // The oneof finding should NOT be populated + s.Nil(violations[0].GetVulnerability()) + s.Nil(violations[0].GetSast()) + s.Nil(violations[0].GetLicenseViolation()) + }, + }, + { + name: "finding_type + string violations - error", + findingType: "VULNERABILITY", + violations: []*engine.PolicyViolation{ + {Subject: "p1", Violation: "plain string"}, + }, + wantErr: "declares finding_type", + }, + { + name: "finding_type + valid structured violations - validates and sets oneof", + findingType: "VULNERABILITY", + violations: []*engine.PolicyViolation{ + {Subject: "p1", Violation: "vuln found", RawFinding: map[string]any{ + "message": "vuln found", "external_id": "CVE-2024-1234", + "package_purl": "pkg:golang/example.com/lib@v1.0.0", "severity": "HIGH", + }}, + }, + wantViolations: 1, + checkFn: func(violations []*v1.PolicyEvaluation_Violation) { + f := violations[0].GetVulnerability() + s.Require().NotNil(f) + s.Equal("CVE-2024-1234", f.GetExternalId()) + s.Equal("HIGH", f.GetSeverity()) + }, + }, + { + name: "finding_type SAST + valid structured violations", + findingType: "SAST", + violations: []*engine.PolicyViolation{ + {Subject: "p1", Violation: "sql injection", RawFinding: map[string]any{ + "message": "sql injection", "rule_id": "java:S1234", + "severity": "HIGH", "location": "src/Handler.java", + }}, + }, + wantViolations: 1, + checkFn: func(violations []*v1.PolicyEvaluation_Violation) { + f := violations[0].GetSast() + s.Require().NotNil(f) + s.Equal("java:S1234", f.GetRuleId()) + }, + }, + { + name: "finding_type + invalid structured violations - validation error", + findingType: "VULNERABILITY", + violations: []*engine.PolicyViolation{ + {Subject: "p1", Violation: "vuln found", RawFinding: map[string]any{ + "message": "vuln found", "external_id": "CVE-1", + // missing package_purl and severity (required) + }}, + }, + wantErr: "validation failed", + }, + { + name: "unknown finding_type - error", + findingType: "UNKNOWN", + violations: []*engine.PolicyViolation{ + {Subject: "p1", Violation: "something", RawFinding: map[string]any{ + "message": "something", + }}, + }, + wantErr: "unknown finding type", + }, + { + name: "empty violations - no error", + findingType: "VULNERABILITY", + violations: []*engine.PolicyViolation{}, + wantViolations: 0, + }, + } + + for _, tc := range cases { + s.Run(tc.name, func() { + results := []*engine.EvaluationResult{ + {Violations: tc.violations}, + } + + violations, warnings, err := engineEvaluationsToAPIViolations(results, tc.findingType) + if tc.wantErr != "" { + s.Require().Error(err) + s.Contains(err.Error(), tc.wantErr) + return + } + + s.Require().NoError(err) + s.Len(violations, tc.wantViolations) + s.Len(warnings, tc.wantWarnings) + if tc.checkFn != nil { + tc.checkFn(violations) + } + }) + } +}