@@ -12,6 +12,19 @@ export const getZeroTrustLists = () =>
12
12
method : "GET" ,
13
13
} ) ;
14
14
15
+ /**
16
+ * Gets Zero Trust list items
17
+ *
18
+ * API docs: https://developers.cloudflare.com/api/operations/zero-trust-lists-zero-trust-list-items
19
+ * @param {string } id The id of the list.
20
+ * @returns {Promise<Object> }
21
+ */
22
+ const getZeroTrustListItems = ( id ) =>
23
+ requestGateway ( `/lists/${ id } /items?per_page=${ LIST_ITEM_SIZE } ` , {
24
+ method : "GET" ,
25
+ } ) ;
26
+
27
+
15
28
/**
16
29
* Creates a Zero Trust list.
17
30
*
@@ -31,14 +44,141 @@ const createZeroTrustList = (name, items) =>
31
44
} ) ,
32
45
} ) ;
33
46
47
+ /**
48
+ * Patches an existing list. Remove/append entries to the list.
49
+ *
50
+ * API docs: https://developers.cloudflare.com/api/operations/zero-trust-lists-patch-zero-trust-list
51
+ * @param {string } listId The ID of the list to patch
52
+ * @param {Object } patch The changes to make
53
+ * @param {string[] } patch.remove A list of the item values you want to remove.
54
+ * @param {Object[] } patch.append Items to add to the list.
55
+ * @param {string } patch.append[].value The domain of an entry.
56
+ * @returns
57
+ */
58
+ const patchExistingList = ( listId , patch ) =>
59
+ requestGateway ( `/lists/${ listId } ` , {
60
+ method : "PATCH" ,
61
+ body : JSON . stringify ( patch ) ,
62
+ } ) ;
63
+
64
+ /**
65
+ * Synchronize Zero Trust lists.
66
+ * Inspects existing lists starting with "CGPS List"
67
+ * Compares the entries in the lists with the desired domains in the items.
68
+ * Removes any entries in the lists that are not in the items.
69
+ * Adds any entries that are in the items and not in the lists.
70
+ * Uses available capacity in existing lists prior to creating a new list.
71
+ * @param {string[] } items The domains.
72
+ */
73
+ export const synchronizeZeroTrustLists = async ( items ) => {
74
+ const itemSet = new Set ( items ) ;
75
+
76
+ console . log ( "Checking existing lists..." ) ;
77
+ const { result : lists } = await getZeroTrustLists ( ) ;
78
+ const cgpsLists = lists ?. filter ( ( { name } ) => name . startsWith ( "CGPS List" ) ) || [ ] ;
79
+ console . log ( `Found ${ cgpsLists . length } existing lists. Calculating diffs...` ) ;
80
+
81
+ const domainsByList = { } ;
82
+ // Do this sequentially to avoid rate-limits
83
+ for ( const list of cgpsLists ) {
84
+ const { result : listItems , result_info } = await getZeroTrustListItems ( list . id ) ;
85
+ if ( result_info . total_count > LIST_ITEM_SIZE ) {
86
+ console . log ( `List ${ list . name } contains more entries that LIST_ITEM_SIZE. Checking only the first ${ LIST_ITEM_SIZE } entires. You may want to delete this list and recreate using the same size limit.` ) ;
87
+ }
88
+ domainsByList [ list . id ] = listItems ?. map ( item => item . value ) || [ ] ;
89
+ }
90
+
91
+ // Extract all the list entries into a map, keyed by domain, pointing to the list.
92
+ const existingDomains = Object . fromEntries (
93
+ Object . entries ( domainsByList ) . flatMap ( ( [ id , domains ] ) => domains . map ( d => [ d , id ] ) )
94
+ ) ;
95
+
96
+ // Create a list of entries to remove.
97
+ // Iterate the existing list(s) removing anything that's in the new list.
98
+ // Resulting in entries that are in the existing list(s) and not in the new list.
99
+ const toRemove = Object . fromEntries (
100
+ Object . entries ( existingDomains ) . filter ( ( [ domain ] ) => ! itemSet . has ( domain ) )
101
+ ) ;
102
+
103
+ // Create a list of entries to add.
104
+ // Iterate the new list keeping only entries not in the existing list(s).
105
+ // Resulting in entries that need to be added.
106
+ const toAdd = items . filter ( domain => ! existingDomains [ domain ] ) ;
107
+
108
+ console . log ( `${ Object . keys ( toRemove ) . length } removals, ${ toAdd . length } additions to make` ) ;
109
+
110
+ // Group the removals by list id, so we can make a patch request.
111
+ const removalPatches = Object . entries ( toRemove ) . reduce ( ( acc , [ domain , listId ] ) => {
112
+ acc [ listId ] = acc [ listId ] || { remove : [ ] } ;
113
+ acc [ listId ] . remove . push ( domain ) ;
114
+ return acc ;
115
+ } , { } ) ;
116
+
117
+ // Fill any "gaps" in the lists made by the removals with any additions.
118
+ // If we can fit all the additions into the same lists that we're processing removals
119
+ // we can minimize the number of lists that need to be edited.
120
+ const patches = Object . fromEntries (
121
+ Object . entries ( removalPatches ) . map ( ( [ listId , patch ] ) => {
122
+ // Work out how much "space" is in the list by looking at
123
+ // how many entries there were and how many we're removing.
124
+ const spaceInList = LIST_ITEM_SIZE - ( domainsByList [ listId ] . length - patch . remove . length ) ;
125
+ // Take upto spaceInList entries from the additions into this list.
126
+ const append = Array ( spaceInList )
127
+ . fill ( 0 )
128
+ . map ( ( ) => toAdd . shift ( ) )
129
+ . filter ( Boolean )
130
+ . map ( domain => ( { value : domain } ) ) ;
131
+ return [ listId , { ...patch , append } ] ;
132
+ } )
133
+ ) ;
134
+
135
+ // Are there any more appends remaining?
136
+ if ( toAdd . length ) {
137
+ // Is there any space in any existing lists, other than those we're already patching?
138
+ const unpatchedListIds = Object . keys ( domainsByList ) . filter ( listId => ! patches [ listId ] ) ;
139
+ unpatchedListIds . forEach ( listId => {
140
+ const spaceInList = LIST_ITEM_SIZE - domainsByList [ listId ] . length ;
141
+ if ( spaceInList > 0 ) {
142
+ // Take upto spaceInList entries from the additions into this list.
143
+ const append = Array ( spaceInList )
144
+ . fill ( 0 )
145
+ . map ( ( ) => toAdd . shift ( ) )
146
+ . filter ( Boolean )
147
+ . map ( domain => ( { value : domain } ) ) ;
148
+
149
+ // Add this list edit to the patches
150
+ if ( append . length ) {
151
+ patches [ listId ] = { append } ;
152
+ }
153
+ }
154
+ } ) ;
155
+ }
156
+
157
+ // Process all the patches. Sequentially to avoid rate limits.
158
+ for ( const [ listId , patch ] of Object . entries ( patches ) ) {
159
+ const appends = ! ! patch . append ? patch . append . length : 0 ;
160
+ const removals = ! ! patch . remove ? patch . remove . length : 0 ;
161
+ console . log ( `Updating list "${ cgpsLists . find ( list => list . id === listId ) . name } "${ appends ? `, ${ appends } additions` : '' } ${ removals ? `, ${ removals } removals` : '' } ` ) ;
162
+ await patchExistingList ( listId , patch ) ;
163
+ }
164
+
165
+ // Are there any more appends remaining?
166
+ if ( toAdd . length ) {
167
+ // We'll need to create new list(s)
168
+ const nextListNumber = Math . max ( 0 , ...cgpsLists . map ( list => parseInt ( list . name . replace ( 'CGPS List - Chunk ' , '' ) ) ) . filter ( x => Number . isInteger ( x ) ) ) + 1 ;
169
+ await createZeroTrustListsOneByOne ( toAdd , nextListNumber ) ;
170
+ }
171
+ } ;
172
+
34
173
/**
35
174
* Creates Zero Trust lists sequentially.
36
175
* @param {string[] } items The domains.
176
+ * @param {Number } [startingListNumber] The chunk number to start from when naming lists.
37
177
*/
38
- export const createZeroTrustListsOneByOne = async ( items ) => {
178
+ export const createZeroTrustListsOneByOne = async ( items , startingListNumber = 1 ) => {
39
179
let totalListNumber = Math . ceil ( items . length / LIST_ITEM_SIZE ) ;
40
180
41
- for ( let i = 0 , listNumber = 1 ; i < items . length ; i += LIST_ITEM_SIZE ) {
181
+ for ( let i = 0 , listNumber = startingListNumber ; i < items . length ; i += LIST_ITEM_SIZE ) {
42
182
const chunk = items
43
183
. slice ( i , i + LIST_ITEM_SIZE )
44
184
. map ( ( item ) => ( { value : item } ) ) ;
@@ -153,9 +293,9 @@ export const createZeroTrustRule = async (wirefilterExpression, name = "CGPS Fil
153
293
* Updates a Zero Trust rule.
154
294
*
155
295
* API docs: https://developers.cloudflare.com/api/operations/zero-trust-gateway-rules-update-zero-trust-gateway-rule
156
- * @param {number } id The ID of the rule to be updated.
296
+ * @param {number } id The ID of the rule to be updated.
157
297
* @param {string } wirefilterExpression The expression to be used for the rule.
158
- * @param {string } name The name of the rule.
298
+ * @param {string } name The name of the rule.
159
299
* @param {string[] } filters The filters to be used for the rule.
160
300
* @returns {Promise<Object> }
161
301
*/
0 commit comments