@@ -670,8 +670,10 @@ fn check_resources(governance: &Governance, report: &Report) -> ViolationCategor
670670 severity : Severity :: Error ,
671671 rule : "image_not_allowed" . to_string ( ) ,
672672 message : format ! (
673- "Image \" {}\" not in allowed list for {}" ,
674- image, rule. resource_type
673+ "Image \" {}\" not in allowed list for {}{}" ,
674+ image,
675+ rule. resource_type,
676+ provenance_suffix( r)
675677 ) ,
676678 details : r. path . clone ( ) ,
677679 expected : Some ( rule. allowed_images . join ( "\n " ) ) ,
@@ -693,8 +695,10 @@ fn check_resources(governance: &Governance, report: &Report) -> ViolationCategor
693695 severity : Severity :: Error ,
694696 rule : "resource_subtype_not_allowed" . to_string ( ) ,
695697 message : format ! (
696- "Resource type \" {}\" not in allowed types for {}" ,
697- rt, rule. resource_type
698+ "Resource type \" {}\" not in allowed types for {}{}" ,
699+ rt,
700+ rule. resource_type,
701+ provenance_suffix( r)
698702 ) ,
699703 details : r. path . clone ( ) ,
700704 expected : Some ( rule. allowed_types . join ( "\n " ) ) ,
@@ -854,8 +858,9 @@ fn check_resolution(report: &Report, scan: &ScanSettings) -> ViolationCategory {
854858 severity : Severity :: Info ,
855859 rule : "vault_unsupported" . to_string ( ) ,
856860 message : format ! (
857- "Template {} is ansible-vault encrypted and was not rendered" ,
858- r. path. as_deref( ) . unwrap_or( "-" )
861+ "Template {} is ansible-vault encrypted and was not rendered{}" ,
862+ r. path. as_deref( ) . unwrap_or( "-" ) ,
863+ provenance_suffix( r)
859864 ) ,
860865 details : Some (
861866 "composit does not decrypt vault files. Role constraints on \
@@ -1145,6 +1150,25 @@ fn string_list(items: &[String]) -> String {
11451150 items. join ( "\n " )
11461151}
11471152
1153+ /// Issue #20: when a resource carries a `provenance` block (set by the
1154+ /// post-scan `apply_provenance` pass), append `(source: <kind> <ref>)` to
1155+ /// the violation message so reports point operators to the upstream spec
1156+ /// rather than the generated artefact. Empty string when no provenance
1157+ /// is present, so callers can append unconditionally.
1158+ fn provenance_suffix ( r : & Resource ) -> String {
1159+ let Some ( prov) = r. extra . get ( "provenance" ) . and_then ( |v| v. as_object ( ) ) else {
1160+ return String :: new ( ) ;
1161+ } ;
1162+ let kind = prov. get ( "source_kind" ) . and_then ( |v| v. as_str ( ) ) ;
1163+ let source_ref = prov. get ( "source_ref" ) . and_then ( |v| v. as_str ( ) ) ;
1164+ match ( kind, source_ref) {
1165+ ( Some ( k) , Some ( rf) ) => format ! ( " (source: {} {})" , k, rf) ,
1166+ ( Some ( k) , None ) => format ! ( " (source: {})" , k) ,
1167+ ( None , Some ( rf) ) => format ! ( " (source: {})" , rf) ,
1168+ ( None , None ) => String :: new ( ) ,
1169+ }
1170+ }
1171+
11481172/// Human-readable summary of which resources a role matched. Used in the
11491173/// `details` field of role violations so the HTML diff shows authors
11501174/// *which* services tripped the rule, not just how many.
@@ -1248,8 +1272,10 @@ fn check_role_constraints(
12481272 severity : Severity :: Error ,
12491273 rule : "role_image_not_pinned" . to_string ( ) ,
12501274 message : format ! (
1251- "Role \" {}\" : image \" {}\" not in pinned list" ,
1252- role. name, image
1275+ "Role \" {}\" : image \" {}\" not in pinned list{}" ,
1276+ role. name,
1277+ image,
1278+ provenance_suffix( r)
12531279 ) ,
12541280 details : Some ( detail. clone ( ) ) ,
12551281 expected : Some ( string_list ( & role. image_pin ) ) ,
@@ -1270,8 +1296,10 @@ fn check_role_constraints(
12701296 severity : Severity :: Error ,
12711297 rule : "role_image_prefix_mismatch" . to_string ( ) ,
12721298 message : format ! (
1273- "Role \" {}\" : image \" {}\" does not match any allowed prefix" ,
1274- role. name, image
1299+ "Role \" {}\" : image \" {}\" does not match any allowed prefix{}" ,
1300+ role. name,
1301+ image,
1302+ provenance_suffix( r)
12751303 ) ,
12761304 details : Some ( detail. clone ( ) ) ,
12771305 expected : Some ( string_list ( & role. image_prefix ) ) ,
@@ -1297,8 +1325,11 @@ fn check_role_constraints(
12971325 severity : Severity :: Error ,
12981326 rule : "role_port_missing" . to_string ( ) ,
12991327 message : format ! (
1300- "Role \" {}\" : required ports {:?} not exposed (missing {:?})" ,
1301- role. name, role. must_expose, missing
1328+ "Role \" {}\" : required ports {:?} not exposed (missing {:?}){}" ,
1329+ role. name,
1330+ role. must_expose,
1331+ missing,
1332+ provenance_suffix( r)
13021333 ) ,
13031334 details : Some ( detail. clone ( ) ) ,
13041335 expected : Some (
@@ -1336,13 +1367,14 @@ fn check_role_constraints(
13361367 severity : Severity :: Error ,
13371368 rule : "role_network_missing" . to_string ( ) ,
13381369 message : format ! (
1339- "Role \" {}\" : not attached to required networks ({})" ,
1370+ "Role \" {}\" : not attached to required networks ({}){} " ,
13401371 role. name,
13411372 missing
13421373 . iter( )
13431374 . map( |s| s. as_str( ) )
13441375 . collect:: <Vec <_>>( )
1345- . join( ", " )
1376+ . join( ", " ) ,
1377+ provenance_suffix( r)
13461378 ) ,
13471379 details : Some ( detail. clone ( ) ) ,
13481380 expected : Some ( string_list ( & role. must_attach_to ) ) ,
@@ -1371,13 +1403,14 @@ fn check_role_constraints(
13711403 severity : Severity :: Error ,
13721404 rule : "role_env_var_missing" . to_string ( ) ,
13731405 message : format ! (
1374- "Role \" {}\" : env vars not set: {}" ,
1406+ "Role \" {}\" : env vars not set: {}{} " ,
13751407 role. name,
13761408 missing
13771409 . iter( )
13781410 . map( |s| s. as_str( ) )
13791411 . collect:: <Vec <_>>( )
1380- . join( ", " )
1412+ . join( ", " ) ,
1413+ provenance_suffix( r)
13811414 ) ,
13821415 details : Some ( detail. clone ( ) ) ,
13831416 expected : Some ( string_list ( & role. must_set_env ) ) ,
@@ -1402,13 +1435,14 @@ fn check_role_constraints(
14021435 severity : Severity :: Error ,
14031436 rule : "role_env_var_forbidden" . to_string ( ) ,
14041437 message : format ! (
1405- "Role \" {}\" : forbidden env vars present: {}" ,
1438+ "Role \" {}\" : forbidden env vars present: {}{} " ,
14061439 role. name,
14071440 present
14081441 . iter( )
14091442 . map( |s| s. as_str( ) )
14101443 . collect:: <Vec <_>>( )
1411- . join( ", " )
1444+ . join( ", " ) ,
1445+ provenance_suffix( r)
14121446 ) ,
14131447 details : Some ( detail. clone ( ) ) ,
14141448 expected : Some ( string_list ( & role. forbidden_env ) ) ,
@@ -1478,8 +1512,9 @@ fn check_role_constraints(
14781512 severity : Severity :: Error ,
14791513 rule : "template_value_mismatch" . to_string ( ) ,
14801514 message : format ! (
1481- "Role \" {}\" : template rendering ({}) key \" {}\" does not satisfy \" {}\" " ,
1482- role. name, src_tag, key, expected_glob
1515+ "Role \" {}\" : template rendering ({}) key \" {}\" does not satisfy \" {}\" {}" ,
1516+ role. name, src_tag, key, expected_glob,
1517+ provenance_suffix( r)
14831518 ) ,
14841519 details : Some ( format ! ( "{} @ {}" , role_tag, path) ) ,
14851520 expected : Some ( format ! ( "{} = {}" , key, expected_glob) ) ,
0 commit comments