2026-09-14 09:54:09 +02:00
package policy
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"gopkg.in/yaml.v3"
)
// POLICY-ATTRIBUTE-UNDECLARED is reported when a package reads
// input.{resource,subject}.attributes.<key> that no sibling registry or
// manifest supplies. The key then resolves only from caller input
// (FLEX-WP-0025-T03 / FLEX-DEC-2026-012).
const undeclaredAttributeCode = "POLICY-ATTRIBUTE-UNDECLARED"
var (
2026-10-10 15:29:00 +02:00
attrDot = regexp . MustCompile ( ` input\.(resource|subject)\.attributes\.([A-Za-z_][A-Za-z0-9_]*) ` )
attrGet = regexp . MustCompile ( ` object\.get\(\s*input\.(resource|subject)\.attributes\s*,\s*"([A-Za-z_][A-Za-z0-9_]*)" ` )
2026-09-14 09:54:09 +02:00
attrNestedGet = regexp . MustCompile ( ` object\.get\(\s*object\.get\(\s*input\.(resource|subject)\s*,\s*"attributes"\s*,\s*\ { \}\s*\)\s*,\s*"([A-Za-z_][A-Za-z0-9_]*)" ` )
)
type attributeRead struct {
Kind string
Key string
}
func ( p * Package ) undeclaredAttributeDiagnostics ( ) [ ] Diagnostic {
reads := attributeReads ( p . RegoModule )
if len ( reads ) == 0 {
return nil
}
dir := filepath . Dir ( p . Source )
if dir == "." || dir == "" || ! filepath . IsAbs ( p . Source ) && ! fileExists ( p . Source ) {
// Inline documents have no sibling registry. Skip unless the source
// path points at a real file (LoadAndValidateFile).
if _ , err := os . Stat ( p . Source ) ; err != nil {
return nil
}
}
supplied := siblingSuppliedAttributes ( filepath . Dir ( p . Source ) )
var diagnostics [ ] Diagnostic
seen := map [ string ] bool { }
for _ , read := range reads {
id := read . Kind + "." + read . Key
if seen [ id ] {
continue
}
seen [ id ] = true
if supplied [ read . Kind ] [ read . Key ] {
continue
}
diagnostics = append ( diagnostics , Diagnostic {
Code : undeclaredAttributeCode ,
Severity : "error" ,
Message : fmt . Sprintf ( "package reads input.%s.attributes.%s, which no sibling registry or manifest supplies; the value can only come from the caller" , read . Kind , read . Key ) ,
Fields : [ ] string { "input." + read . Kind + ".attributes." + read . Key } ,
Metadata : map [ string ] any {
"kind" : read . Kind ,
"key" : read . Key ,
} ,
} )
}
sort . Slice ( diagnostics , func ( i , j int ) bool {
return diagnostics [ i ] . Message < diagnostics [ j ] . Message
} )
return diagnostics
}
func attributeReads ( regoModule string ) [ ] attributeRead {
var reads [ ] attributeRead
add := func ( kind , key string ) {
reads = append ( reads , attributeRead { Kind : kind , Key : key } )
}
for _ , re := range [ ] * regexp . Regexp { attrDot , attrGet , attrNestedGet } {
for _ , match := range re . FindAllStringSubmatch ( regoModule , - 1 ) {
add ( match [ 1 ] , match [ 2 ] )
}
}
return reads
}
func siblingSuppliedAttributes ( dir string ) map [ string ] map [ string ] bool {
out := map [ string ] map [ string ] bool {
"resource" : { } ,
"subject" : { } ,
}
names := [ ] string {
"registry_snapshot.json" ,
"production_registry_snapshot.json" ,
"registry.json" ,
"resource_manifest.yaml" ,
"subject_manifest.yaml" ,
}
for _ , name := range names {
path := filepath . Join ( dir , name )
data , err := os . ReadFile ( path )
if err != nil {
continue
}
var doc any
if strings . HasSuffix ( name , ".json" ) {
if err := json . Unmarshal ( data , & doc ) ; err != nil {
continue
}
} else if err := yaml . Unmarshal ( data , & doc ) ; err != nil {
continue
}
collectSuppliedAttributes ( doc , out )
}
return out
}
func collectSuppliedAttributes ( doc any , out map [ string ] map [ string ] bool ) {
obj , ok := asMap ( doc )
if ! ok {
return
}
if resources , ok := asList ( obj [ "resources" ] ) ; ok {
for _ , item := range resources {
collectResourceKeys ( item , out [ "resource" ] )
}
}
if manifests , ok := asList ( obj [ "resource_manifests" ] ) ; ok {
for _ , item := range manifests {
collectSuppliedAttributes ( item , out )
}
}
if subjects , ok := asList ( obj [ "subjects" ] ) ; ok {
for _ , item := range subjects {
collectSubjectKeys ( item , out [ "subject" ] )
}
}
}
func asList ( value any ) ( [ ] any , bool ) {
switch typed := value . ( type ) {
case [ ] any :
return typed , true
default :
return nil , false
}
}
func collectResourceKeys ( item any , keys map [ string ] bool ) {
obj , ok := asMap ( item )
if ! ok {
return
}
// First-class fields enrichment copies into attributes.
for _ , key := range [ ] string { "path" , "parent" , "labels" , "trust_zone" , "owner" } {
keys [ key ] = true
}
addMapKeys ( obj [ "attributes" ] , keys )
}
func collectSubjectKeys ( item any , keys map [ string ] bool ) {
obj , ok := asMap ( item )
if ! ok {
return
}
for _ , key := range [ ] string { "display_name" , "organization_relation" , "roles" , "groups" } {
keys [ key ] = true
}
addMapKeys ( obj [ "attributes" ] , keys )
addMapKeys ( obj [ "claims" ] , keys )
addMapKeys ( obj [ "metadata" ] , keys )
}
func addMapKeys ( value any , keys map [ string ] bool ) {
obj , ok := asMap ( value )
if ! ok {
return
}
for key := range obj {
keys [ key ] = true
}
}
func asMap ( value any ) ( map [ string ] any , bool ) {
switch typed := value . ( type ) {
case map [ string ] any :
return typed , true
case map [ any ] any :
return stringifyKeys ( typed ) , true
default :
return nil , false
}
}
func stringifyKeys ( in map [ any ] any ) map [ string ] any {
out := make ( map [ string ] any , len ( in ) )
for key , value := range in {
out [ fmt . Sprint ( key ) ] = value
}
return out
}
func fileExists ( path string ) bool {
_ , err := os . Stat ( path )
return err == nil
}