@@ -11,64 +11,237 @@ class TagProtectionChecks {
1111 }
1212 async checkTagProtection ( ) {
1313 const rulesets = await ( 0 , Repositories_1 . getRepoRulesets ) ( this . repository . owner , this . repository . name ) ;
14- // Filter to get only tag rulesets that are active
15- const tagRulesets = rulesets . filter ( ( ruleset ) => ruleset . target === "tag" && ruleset . enforcement === "active" ) ;
16- const policyTags = this . policy . protected_tags || [ ] ;
17- const protectedTagPatterns = tagRulesets
18- . map ( ( ruleset ) => {
19- // Extract tag patterns from conditions
20- if ( ruleset . conditions ?. ref_name ?. include ) {
21- return ruleset . conditions . ref_name . include ;
22- }
23- return [ ] ;
24- } )
25- . flat ( ) ;
26- // Check which policy tags are protected
27- const passedTags = [ ] ;
28- const failedTags = [ ] ;
29- for ( const policyTag of policyTags ) {
30- const isProtected = this . isTagProtected ( policyTag . name , protectedTagPatterns ) ;
31- if ( isProtected ) {
32- passedTags . push ( policyTag . name ) ;
33- }
34- else {
35- failedTags . push ( policyTag . name ) ;
36- }
14+ const policyTags = this . policy . tags ;
15+ if ( ! policyTags ) {
16+ return this . createResult ( true , { } , { } ) ;
17+ }
18+ // Filter to get only tag rulesets that match the policy enforcement
19+ const tagRulesets = rulesets . filter ( ( ruleset ) => ruleset . target === "tag" &&
20+ ruleset . enforcement === policyTags . enforcement ) ;
21+ const checks = {
22+ enforcement : { passed : false , details : { } } ,
23+ scope : { passed : false , details : { } } ,
24+ operations : { passed : false , details : { } } ,
25+ naming : { passed : false , details : { } } ,
26+ bypass : { passed : false , details : { } } ,
27+ } ;
28+ // Check if we found matching rulesets
29+ if ( tagRulesets . length === 0 ) {
30+ checks . enforcement . passed = false ;
31+ checks . enforcement . details = {
32+ expected : policyTags . enforcement ,
33+ found : "none" ,
34+ } ;
35+ return this . createResult ( false , checks , {
36+ message : "No tag rulesets found with required enforcement level" ,
37+ } ) ;
38+ }
39+ // For simplicity, we'll check the first matching ruleset
40+ // In production, you might want to check all or aggregate results
41+ const ruleset = tagRulesets [ 0 ] ;
42+ // Check enforcement
43+ checks . enforcement . passed = ruleset . enforcement === policyTags . enforcement ;
44+ checks . enforcement . details = {
45+ expected : policyTags . enforcement ,
46+ actual : ruleset . enforcement ,
47+ } ;
48+ // Check scope (include/exclude patterns)
49+ checks . scope = this . checkScope ( policyTags . scope , ruleset . conditions ) ;
50+ // Check operations (create/update/delete)
51+ checks . operations = this . checkOperations ( policyTags . operations , ruleset ) ;
52+ // Check naming constraints
53+ if ( policyTags . naming ?. enabled ) {
54+ checks . naming = this . checkNaming ( policyTags . naming , ruleset ) ;
55+ }
56+ else {
57+ checks . naming . passed = true ;
58+ checks . naming . details = { message : "Naming constraints not enabled" } ;
59+ }
60+ // Check bypass actors
61+ if ( policyTags . bypass ) {
62+ checks . bypass = this . checkBypass ( policyTags . bypass , ruleset ) ;
3763 }
38- return this . createResult ( passedTags , failedTags , protectedTagPatterns ) ;
64+ else {
65+ checks . bypass . passed = true ;
66+ checks . bypass . details = { message : "Bypass not configured in policy" } ;
67+ }
68+ const allPassed = checks . enforcement . passed &&
69+ checks . scope . passed &&
70+ checks . operations . passed &&
71+ checks . naming . passed &&
72+ checks . bypass . passed ;
73+ return this . createResult ( allPassed , checks , {
74+ ruleset_id : ruleset . id ,
75+ ruleset_name : ruleset . name ,
76+ } ) ;
77+ }
78+ checkScope ( policyScope , conditions ) {
79+ const includePatterns = conditions ?. ref_name ?. include || [ ] ;
80+ const excludePatterns = conditions ?. ref_name ?. exclude || [ ] ;
81+ const expectedInclude = policyScope . include || [ ] ;
82+ const expectedExclude = policyScope . exclude || [ ] ;
83+ // Check if all expected include patterns are present
84+ const missingIncludes = expectedInclude . filter ( ( pattern ) => ! includePatterns . includes ( pattern ) ) ;
85+ // Check if all expected exclude patterns are present
86+ const missingExcludes = expectedExclude . filter ( ( pattern ) => ! excludePatterns . includes ( pattern ) ) ;
87+ const passed = missingIncludes . length === 0 && missingExcludes . length === 0 ;
88+ return {
89+ passed,
90+ details : {
91+ expected_include : expectedInclude ,
92+ actual_include : includePatterns ,
93+ missing_includes : missingIncludes ,
94+ expected_exclude : expectedExclude ,
95+ actual_exclude : excludePatterns ,
96+ missing_excludes : missingExcludes ,
97+ } ,
98+ } ;
3999 }
40- isTagProtected ( tagName , protectedPatterns ) {
41- // Check if tag name matches any protected pattern
42- for ( const pattern of protectedPatterns ) {
43- if ( pattern === "~ALL" ) {
44- return true ;
100+ checkOperations ( policyOps , ruleset ) {
101+ // Extract operation rules from ruleset
102+ const rules = ruleset . rules || [ ] ;
103+ const operationRules = {
104+ create : "allowed" ,
105+ update : "allowed" ,
106+ delete : "allowed" ,
107+ } ;
108+ // Check for creation, update, and deletion rules
109+ rules . forEach ( ( rule ) => {
110+ if ( rule . type === "creation" ) {
111+ operationRules . create = "restricted" ;
112+ }
113+ if ( rule . type === "update" ) {
114+ operationRules . update = "restricted" ;
45115 }
46- // Convert GitHub pattern to regex
47- const regexPattern = pattern
48- . replace ( / \* / g, ".*" )
49- . replace ( / \? / g, "." )
50- . replace ( / \[ / g, "\\[" )
51- . replace ( / \] / g, "\\]" ) ;
52- const regex = new RegExp ( `^${ regexPattern } $` ) ;
53- if ( regex . test ( tagName ) ) {
54- return true ;
116+ if ( rule . type === "deletion" ) {
117+ operationRules . delete = "restricted" ;
55118 }
119+ } ) ;
120+ const checks = {
121+ create : operationRules . create === policyOps . create ,
122+ update : operationRules . update === policyOps . update ,
123+ delete : operationRules . delete === policyOps . delete ,
124+ } ;
125+ const passed = checks . create && checks . update && checks . delete ;
126+ return {
127+ passed,
128+ details : {
129+ expected : policyOps ,
130+ actual : operationRules ,
131+ checks,
132+ } ,
133+ } ;
134+ }
135+ checkNaming ( policyNaming , ruleset ) {
136+ // GitHub rulesets don't have a direct naming constraint rule
137+ // This would need to be implemented based on specific ruleset rules
138+ // For now, we'll return a placeholder
139+ return {
140+ passed : true ,
141+ details : {
142+ message : "Naming constraint checking not fully implemented - requires custom rule analysis" ,
143+ policy : policyNaming ,
144+ } ,
145+ } ;
146+ }
147+ checkBypass ( policyBypass , ruleset ) {
148+ const bypassActors = ruleset . bypass_actors || [ ] ;
149+ const checks = { } ;
150+ // Check organization admins
151+ if ( policyBypass . organization_admins ) {
152+ const orgAdminBypass = bypassActors . find ( ( actor ) => actor . actor_type === "OrganizationAdmin" ) ;
153+ checks . organization_admins = {
154+ expected : policyBypass . organization_admins ,
155+ found : orgAdminBypass ? orgAdminBypass . bypass_mode : "not configured" ,
156+ passed : orgAdminBypass ?. bypass_mode === policyBypass . organization_admins ,
157+ } ;
158+ }
159+ // Check teams
160+ if ( policyBypass . teams ) {
161+ checks . teams = policyBypass . teams . map ( ( team ) => {
162+ const teamBypass = bypassActors . find ( ( actor ) => actor . actor_type === "Team" && actor . actor_id === team . id ) ;
163+ return {
164+ id : team . id ,
165+ expected_mode : team . mode ,
166+ actual_mode : teamBypass ? teamBypass . bypass_mode : "not configured" ,
167+ passed : teamBypass ?. bypass_mode === team . mode ,
168+ } ;
169+ } ) ;
170+ }
171+ // Check integrations
172+ if ( policyBypass . integrations ) {
173+ checks . integrations = policyBypass . integrations . map ( ( integration ) => {
174+ const integrationBypass = bypassActors . find ( ( actor ) => actor . actor_type === "Integration" &&
175+ actor . actor_id === integration . id ) ;
176+ return {
177+ id : integration . id ,
178+ expected_mode : integration . mode ,
179+ actual_mode : integrationBypass
180+ ? integrationBypass . bypass_mode
181+ : "not configured" ,
182+ passed : integrationBypass ?. bypass_mode === integration . mode ,
183+ } ;
184+ } ) ;
185+ }
186+ // Check repository roles
187+ if ( policyBypass . repository_roles ) {
188+ checks . repository_roles = policyBypass . repository_roles . map ( ( role ) => {
189+ const roleBypass = bypassActors . find ( ( actor ) => actor . actor_type === "RepositoryRole" &&
190+ actor . actor_id === role . id ) ;
191+ return {
192+ id : role . id ,
193+ expected_mode : role . mode ,
194+ actual_mode : roleBypass ? roleBypass . bypass_mode : "not configured" ,
195+ passed : roleBypass ?. bypass_mode === role . mode ,
196+ } ;
197+ } ) ;
56198 }
57- return false ;
199+ // Check deploy keys
200+ if ( policyBypass . deploy_keys ) {
201+ const deployKeyBypass = bypassActors . find ( ( actor ) => actor . actor_type === "DeployKey" ) ;
202+ checks . deploy_keys = {
203+ expected_allow : policyBypass . deploy_keys . allow ,
204+ expected_mode : policyBypass . deploy_keys . mode ,
205+ found : deployKeyBypass ? "configured" : "not configured" ,
206+ actual_mode : deployKeyBypass
207+ ? deployKeyBypass . bypass_mode
208+ : "not configured" ,
209+ passed : policyBypass . deploy_keys . allow === ! ! deployKeyBypass &&
210+ ( ! deployKeyBypass ||
211+ deployKeyBypass . bypass_mode === policyBypass . deploy_keys . mode ) ,
212+ } ;
213+ }
214+ // Determine if all bypass checks passed
215+ const allBypassPassed = Object . values ( checks ) . every ( ( check ) => {
216+ if ( Array . isArray ( check ) ) {
217+ return check . every ( ( item ) => item . passed ) ;
218+ }
219+ return check . passed ;
220+ } ) ;
221+ return {
222+ passed : allBypassPassed ,
223+ details : checks ,
224+ } ;
58225 }
59- createResult ( passed , failed , protectedPatterns ) {
226+ createResult ( passed , checks , info ) {
60227 const name = "Tag Protection" ;
61- const pass = failed . length === 0 ;
228+ // Determine which checks passed and failed
229+ const passedChecks = [ ] ;
230+ const failedChecks = { } ;
231+ Object . entries ( checks ) . forEach ( ( [ key , value ] ) => {
232+ if ( value . passed ) {
233+ passedChecks . push ( key ) ;
234+ }
235+ else {
236+ failedChecks [ key ] = value . details ;
237+ }
238+ } ) ;
62239 const data = {
63- passed,
64- failed : {
65- not_protected : failed ,
66- } ,
67- info : {
68- protected_patterns : protectedPatterns ,
69- } ,
240+ passed : passedChecks ,
241+ failed : failedChecks ,
242+ info,
70243 } ;
71- return { name, pass, data } ;
244+ return { name, pass : passed , data } ;
72245 }
73246}
74247exports . TagProtectionChecks = TagProtectionChecks ;
0 commit comments