@gfargo/doorman 3.13.7 → 3.13.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/run.js +15 -15
- package/dist/bin/run.mjs +2 -2
- package/package.json +1 -1
package/dist/bin/run.mjs
CHANGED
|
@@ -35,8 +35,8 @@ ${F}`)}),C.result}),{toAdd:l,toUpdate:c,toDelete:d}=this.diffRules(at(o.data.rul
|
|
|
35
35
|
\u2022 Ensure your API token has Account:Read permissions
|
|
36
36
|
\u2022 Verify the account ID is correct
|
|
37
37
|
\u2022 Check token permissions in Cloudflare dashboard`)):n.includes("not found")?(r.warn(`\u{1F6AB} Lists API resource not found during ${t}. Falling back to individual IP rules.`),r.info("\u{1F4A1} This may be temporary - the List will be recreated on next sync if Lists API is available.")):n.includes("rate limit")||n.includes("quota")?(r.warn(`\u{1F6AB} Lists API rate limited during ${t}. Falling back to individual IP rules.`),r.info("\u{1F4A1} Lists API rate limit reached. Individual rules will be used this time. Consider retrying later or upgrading your Cloudflare plan.")):(r.warn(`\u{1F6AB} Lists API error during ${t}: ${e.message}. Falling back to individual IP rules.`),r.info("\u{1F4A1} Lists API temporarily unavailable. Using individual IP rules as fallback. Check Cloudflare status if this persists."))}else r.warn(`\u{1F6AB} Unknown Lists API error during ${t}. Falling back to individual IP rules.`)}};var pt=class{static create(e,t,n){let o=e||process.env.CLOUDFLARE_API_TOKEN,s=t||process.env.CLOUDFLARE_ZONE_ID,a=n||process.env.CLOUDFLARE_ACCOUNT_ID;if(!o)throw new Error("Cloudflare API token is required. Set CLOUDFLARE_API_TOKEN environment variable or pass as parameter.");if(!s)throw new Error("Cloudflare Zone ID is required. Set CLOUDFLARE_ZONE_ID environment variable or pass as parameter.");return new ut(o,s,a)}static fromEnv(){return this.create()}static fromConfig(e){return this.create(e.apiToken,e.zoneId,e.accountId)}};T();T();var Ln=process.env.DOORMAN_FASTLY_API_BASE_URL||"https://api.fastly.com",ft="doorman-managed-deny",gt="doorman-managed-allow",Ye=class extends Te{constructor(t,n){super(Ln,"fastly");this.workspaceId=t;this.apiToken=n}workspaceId;apiToken;async getAuthHeaders(){return{"Fastly-Key":this.apiToken}}workspacePath(...t){return["/ngwaf/v1/workspaces",this.workspaceId,...t].join("/")}async fetchRules(){return(await this.get(this.workspacePath("rules"))).data}async createRule(t){return this.post(this.workspacePath("rules"),t,{retries:0})}async updateRule(t,n){return this.patch(this.workspacePath("rules",t),n)}async deleteRule(t){await this.delete(this.workspacePath("rules",t))}async fetchLists(){return(await this.get(this.workspacePath("lists"))).data}async createList(t,n){return this.post(this.workspacePath("lists"),{name:t,type:"ip",description:"Managed by doorman \u2014 do not edit entries directly, they will be overwritten on the next sync.",entries:n},{retries:0})}async updateListEntries(t,n){return this.patch(this.workspacePath("lists",t),{entries:n})}async findList(t){return(await this.fetchLists()).find(o=>o.name===t&&o.type==="ip")??null}async getOrCreateList(t){let n=await this.findList(t);return n||(r.debug(`Creating Fastly list "${t}" in workspace ${this.workspaceId}`),this.createList(t,[]))}async fetchWorkspace(){return this.get(this.workspacePath())}async verifyCredentials(){try{return await this.fetchWorkspace(),!0}catch(t){return r.debug("Credential verification failed:",t),!1}}};T();var Je=class extends $e{constructor(t){super();this.client=t}client;name="fastly";async fetchConfig(){try{r.debug("Fetching Fastly Next-Gen WAF configuration");let[t,n,o]=await Promise.all([this.client.fetchRules(),this.client.findList(ft),this.client.findList(gt)]),s=this.translateFastlyRules(t),a=[...A.fastlyListEntriesToUnified(n?.entries??[],"deny"),...A.fastlyListEntriesToUnified(o?.entries??[],"allow")];return{version:"2.0",provider:"fastly",providers:{fastly:{workspaceId:this.client.workspaceId}},rules:s,ips:a}}catch(t){throw r.error("Error fetching Fastly configuration:",t),new Error("Failed to fetch Fastly Next-Gen WAF configuration")}}translateFastlyRules(t){let n=[];for(let o of t){if(o.type!=="request"&&o.type!=="rate_limit"){r.debug(`Skipping Fastly rule ${o.id} \u2014 type "${o.type}" is not a request/rate_limit rule doorman manages`);continue}let s=A.fastlyToUnified(o);s.warnings.length>0&&s.warnings.forEach(a=>{let{TranslationWarningSystem:l}=(de(),Re(Oe));r.warn(`Rule ${o.description||o.id}:
|
|
38
|
-
${l.formatWarning(a)}`)}),n.push(s.result)}return n}async syncRules(t,n={}){let{dryRun:o=!1}=n;try{let{OperationSafety:s}=(ct(),Re(lt)),a=await s.performDryRunValidation(t,"sync rules",async m=>this.getChanges(m));if(!a.valid)throw new Error(`Dry-run validation failed: ${a.issues.join(", ")}`);let l=a.changes,{rulesToAdd:c,rulesToUpdate:d,rulesToDelete:u}=l,p=l.ipsToAdd||[],f=l.ipsToDelete||[];if(o)return r.info("Dry run mode. The following changes would be made:"),r.info(`Custom Rules - Add: ${c.length}, Update: ${d.length}, Delete: ${u.length}`),r.info(`IP Rules - Add: ${p.length}, Delete: ${f.length}`),{success:!0,rulesAdded:0,rulesUpdated:0,rulesDeleted:0,ipsAdded:0,ipsUpdated:0,ipsDeleted:0,warnings:a.warnings};let y=s.assessOperationRisk(l,t);if(!await s.confirmDestructiveOperation({operation:"sync rules",target:`Fastly workspace ${this.client.workspaceId}`,changes:l,riskLevel:y,skipConfirmation:n.force||!1,dryRun:!1,allowDeletions:n.allowDeletions||!1}))throw new Error("Operation cancelled by user");let C=[];Pt(t.rules)&&C.push("Rule `priority` has no effect on Fastly: Next-Gen WAF rules are evaluated independently and have no API-exposed ordering concept to honour.");let x=c.map(m=>({rule:m,translated:A.unifiedToFastly(m)})),O=d.map(m=>({rule:m,translated:A.unifiedToFastly(m)})),F=[],se=(m,$)=>`${m}: ${$ instanceof Error?$.message:String($)}`,ae=[],fe=[],k=[],le=[];for(let m of u)if(m.id)try{r.debug(`Deleting Fastly rule: ${m.id}`),await this.client.deleteRule(m.id),k.push(m.id)}catch($){r.error(`Failed to delete Fastly rule ${m.id}:`,$),F.push(se(`Failed to delete rule ${m.id}`,$))}for(let{rule:m,translated:$}of x){let D=$.result;try{r.debug(`Adding new Fastly rule: ${D.description}`);let he=await this.client.createRule(D);ae.push(he),le.push({oldId:m.id||void 0,newId:he.id,name:D.description})}catch(he){r.error(`Failed to add Fastly rule "${D.description}":`,he),F.push(se(`Failed to add rule "${D.description}"`,he))}}for(let{rule:m,translated:$}of O)if(m.id)try{r.debug(`Updating Fastly rule: ${m.id}`);let D=await this.client.updateRule(m.id,$.result);fe.push(D)}catch(D){r.error(`Failed to update Fastly rule ${m.id}:`,D),F.push(se(`Failed to update rule ${m.id}`,D))}let ge=0,me=0,Ie=t.ips||[],{result:we,warnings:P}=A.unifiedIPsToFastlyEntries(Ie);P.forEach(m=>C.push(m.message));try{for(let[m,$]of[[ft,we.denyEntries],[gt,we.allowEntries]]){let he=(await this.client.findList(m))?.entries??[],it=$.filter(g=>!he.includes(g)),nt=he.filter(g=>!$.includes(g));if(it.length===0&&nt.length===0)continue;let xe=await this.client.getOrCreateList(m);await this.client.updateListEntries(xe.id,$),ge+=it.length,me+=nt.length}}catch(m){r.error("Failed to sync Fastly IP lists:",m),F.push(se("Failed to sync IP lists",m))}return r.debug(`Custom Rules: Added: ${ae.length}, Updated: ${fe.length}, Deleted: ${k.length}`),r.debug(`IP Rules: Added: ${ge}, Deleted: ${me}`),{success:F.length===0,rulesAdded:ae.length,rulesUpdated:fe.length,rulesDeleted:k.length,ipsAdded:ge,ipsUpdated:0,ipsDeleted:me,idRemappings:le.length>0?le:void 0,errors:F.length>0?F:void 0,warnings:C.length>0?C:void 0}}catch(s){throw r.error("Error during sync:",s),new Error(`Failed to synchronize firewall rules: ${s instanceof Error?s.message:String(s)}`,{cause:s})}}async getChanges(t){let n=_e.safeParse(t);if(!n.success)throw new Error(`Invalid firewall configuration: ${n.error.message}`);try{r.debug("Fetching existing Fastly configuration");let[o,s,a]=await Promise.all([this.client.fetchRules(),this.client.findList(ft),this.client.findList(gt)]),l=this.translateFastlyRules(o),{toAdd:c,toUpdate:d,toDelete:u}=this.diffRules(n.data.rules,l),p=s?.entries??[],f=a?.entries??[],y=[...A.fastlyListEntriesToUnified(p,"deny"),...A.fastlyListEntriesToUnified(f,"allow")],{ipsToAdd:w,ipsToDelete:C}=this.diffIPs(n.data.ips||[],y);return{rulesToAdd:c,rulesToUpdate:d,rulesToDelete:u,ipsToAdd:w,ipsToUpdate:[],ipsToDelete:C,hasChanges:c.length>0||d.length>0||u.length>0||w.length>0||C.length>0}}catch(o){throw r.error("Error fetching existing Fastly configuration:",o),new Error("Failed to fetch existing Fastly configuration")}}getSupportedFeatures(){return{supportsCustomRules:!0,supportsIPBlocking:!0,supportsRateLimiting:!0,supportsManagedRules:!1,supportsGeoBlocking:!0,supportsRedirect:!0,supportsChallenge:!0}}async verifyCredentials(){return this.client.verifyCredentials()}validateConfig(t){let n=super.validateConfig(t),o=[...n.errors],s=[...n.warnings];return t.provider&&t.provider!=="fastly"&&o.push({path:"provider",message:"Provider must be 'fastly' for FastlyFirewallService",code:"INVALID_PROVIDER"}),{valid:o.length===0,errors:o,warnings:s}}getHealthScore(t){let n=super.getHealthScore(t),o=[...n.issues],s=n.score;return t.rules.filter(l=>l.action?.type==="rate_limit").length===0&&(s-=10,o.push({severity:"info",category:"security",message:"No rate limiting rules configured",suggestion:"Consider adding rate limiting to protect against abuse"})),(!t.ips||t.ips.length===0)&&o.push({severity:"info",category:"security",message:"No IP blocking rules configured",suggestion:"Consider blocking known malicious IPs"}),{score:Math.max(0,s),grade:n.grade,issues:o,recommendations:n.recommendations}}diffRules(t,n){let o=[],s=[],a=[...n];for(let l of t){let c=n.find(d=>d.id===l.id);if(!c)o.push(l);else{Le(pe(l),pe(c))||s.push({...l,id:c.id});let d=a.findIndex(u=>u.id===c.id);d!==-1&&a.splice(d,1)}}return{toAdd:o,toUpdate:s,toDelete:a}}diffIPs(t,n){let o=l=>`${l.action}:${l.ip}`,s=new Set(n.map(o)),a=new Set(t.map(o));return{ipsToAdd:t.filter(l=>!s.has(o(l))),ipsToDelete:n.filter(l=>!a.has(o(l)))}}};T();var mt=class{static fromEnv(){let e=process.env.FASTLY_API_TOKEN,t=process.env.FASTLY_WORKSPACE_ID;if(!e)throw new Error("FASTLY_API_TOKEN environment variable is required");if(!t)throw new Error("FASTLY_WORKSPACE_ID environment variable is required");let n=new Ye(t,e);return new Je(n)}static fromConfig(e){let t=e.apiToken||process.env.FASTLY_API_TOKEN,n=e.workspaceId||process.env.FASTLY_WORKSPACE_ID;if(!t)throw new Error("Fastly API token is required (provide apiToken or set FASTLY_API_TOKEN env var)");if(!n)throw new Error("Fastly workspace ID is required (provide workspaceId or set FASTLY_WORKSPACE_ID env var)");r.debug("Creating Fastly provider with config:",{workspaceId:n});let o=new Ye(n,t);return new Je(o)}static create(e,t){return this.fromConfig({workspaceId:e,apiToken:t})}};T();import{GoogleAuth as Jr}from"google-auth-library";var Dn=process.env.DOORMAN_GCP_API_BASE_URL||"https://compute.googleapis.com/compute/v1",Qr="https://www.googleapis.com/auth/compute",Xr=1e3,xn=6e4,Qe=class extends Te{auth;projectId;policyName;constructor(e,t,n){super(`${Dn}/projects/${e}/global`,"gcp"),this.projectId=e,this.policyName=t,this.auth=new Jr({scopes:[Qr],...n?{keyFile:n}:{}})}async getAuthHeaders(){let t=await(await this.auth.getClient()).getRequestHeaders();return Object.fromEntries(t.entries())}policyPath(...e){return["/securityPolicies",this.policyName,...e].join("/")}async getPolicy(){return this.get(this.policyPath())}async addRule(e){let t=await this.post(this.policyPath("addRule"),e);await this.waitForOperation(t)}async patchRule(e,t){let n=await this.post(`${this.policyPath("patchRule")}?priority=${e}`,t);await this.waitForOperation(n)}async removeRule(e){let t=await this.post(`${this.policyPath("removeRule")}?priority=${e}`);await this.waitForOperation(t)}async waitForOperation(e){let t=Date.now()+xn,n=e;for(;n.status!=="DONE";){if(Date.now()>=t)throw new Error(`gcp operation "${n.name}" did not complete within ${xn}ms (last status: ${n.status})`);await this.delay(Xr),n=await this.get(`/operations/${n.name}`)}if(n.httpErrorStatusCode||n.error){let o=n.error?.errors?.map(s=>s.message).join(", ")||n.httpErrorMessage;throw new Error(`gcp operation "${n.name}" failed: ${o||"unknown error"}`)}n.warnings?.length&&n.warnings.forEach(o=>r.warn(`gcp operation "${n.name}" warning: ${o.message}`))}async verifyCredentials(){try{return await this.getPolicy(),!0}catch(e){return r.debug("Credential verification failed:",e),!1}}};T();function kt(i){return i!==void 0&&/^\d+$/.test(i)?Number(i):void 0}var Xe=class extends $e{constructor(t){super();this.client=t}client;name="gcp";async fetchConfig(){try{r.debug("Fetching Cloud Armor security policy");let t=await this.client.getPolicy(),{rules:n,ips:o}=this.translatePolicy(t);return{version:"2.0",provider:"gcp",providers:{gcp:{projectId:this.client.projectId,policyName:this.client.policyName}},rules:n,ips:o}}catch(t){throw r.error("Error fetching Cloud Armor configuration:",t),new Error("Failed to fetch Cloud Armor security policy")}}translatePolicy(t){let
|
|
39
|
-
${
|
|
38
|
+
${l.formatWarning(a)}`)}),n.push(s.result)}return n}async syncRules(t,n={}){let{dryRun:o=!1}=n;try{let{OperationSafety:s}=(ct(),Re(lt)),a=await s.performDryRunValidation(t,"sync rules",async m=>this.getChanges(m));if(!a.valid)throw new Error(`Dry-run validation failed: ${a.issues.join(", ")}`);let l=a.changes,{rulesToAdd:c,rulesToUpdate:d,rulesToDelete:u}=l,p=l.ipsToAdd||[],f=l.ipsToDelete||[];if(o)return r.info("Dry run mode. The following changes would be made:"),r.info(`Custom Rules - Add: ${c.length}, Update: ${d.length}, Delete: ${u.length}`),r.info(`IP Rules - Add: ${p.length}, Delete: ${f.length}`),{success:!0,rulesAdded:0,rulesUpdated:0,rulesDeleted:0,ipsAdded:0,ipsUpdated:0,ipsDeleted:0,warnings:a.warnings};let y=s.assessOperationRisk(l,t);if(!await s.confirmDestructiveOperation({operation:"sync rules",target:`Fastly workspace ${this.client.workspaceId}`,changes:l,riskLevel:y,skipConfirmation:n.force||!1,dryRun:!1,allowDeletions:n.allowDeletions||!1}))throw new Error("Operation cancelled by user");let C=[];Pt(t.rules)&&C.push("Rule `priority` has no effect on Fastly: Next-Gen WAF rules are evaluated independently and have no API-exposed ordering concept to honour.");let x=c.map(m=>({rule:m,translated:A.unifiedToFastly(m)})),O=d.map(m=>({rule:m,translated:A.unifiedToFastly(m)})),F=[],se=(m,$)=>`${m}: ${$ instanceof Error?$.message:String($)}`,ae=[],fe=[],k=[],le=[];for(let m of u)if(m.id)try{r.debug(`Deleting Fastly rule: ${m.id}`),await this.client.deleteRule(m.id),k.push(m.id)}catch($){r.error(`Failed to delete Fastly rule ${m.id}:`,$),F.push(se(`Failed to delete rule ${m.id}`,$))}for(let{rule:m,translated:$}of x){let D=$.result;try{r.debug(`Adding new Fastly rule: ${D.description}`);let he=await this.client.createRule(D);ae.push(he),le.push({oldId:m.id||void 0,newId:he.id,name:D.description})}catch(he){r.error(`Failed to add Fastly rule "${D.description}":`,he),F.push(se(`Failed to add rule "${D.description}"`,he))}}for(let{rule:m,translated:$}of O)if(m.id)try{r.debug(`Updating Fastly rule: ${m.id}`);let D=await this.client.updateRule(m.id,$.result);fe.push(D)}catch(D){r.error(`Failed to update Fastly rule ${m.id}:`,D),F.push(se(`Failed to update rule ${m.id}`,D))}let ge=0,me=0,Ie=t.ips||[],{result:we,warnings:P}=A.unifiedIPsToFastlyEntries(Ie);P.forEach(m=>C.push(m.message));try{for(let[m,$]of[[ft,we.denyEntries],[gt,we.allowEntries]]){let he=(await this.client.findList(m))?.entries??[],it=$.filter(g=>!he.includes(g)),nt=he.filter(g=>!$.includes(g));if(it.length===0&&nt.length===0)continue;let xe=await this.client.getOrCreateList(m);await this.client.updateListEntries(xe.id,$),ge+=it.length,me+=nt.length}}catch(m){r.error("Failed to sync Fastly IP lists:",m),F.push(se("Failed to sync IP lists",m))}return r.debug(`Custom Rules: Added: ${ae.length}, Updated: ${fe.length}, Deleted: ${k.length}`),r.debug(`IP Rules: Added: ${ge}, Deleted: ${me}`),{success:F.length===0,rulesAdded:ae.length,rulesUpdated:fe.length,rulesDeleted:k.length,ipsAdded:ge,ipsUpdated:0,ipsDeleted:me,idRemappings:le.length>0?le:void 0,errors:F.length>0?F:void 0,warnings:C.length>0?C:void 0}}catch(s){throw r.error("Error during sync:",s),new Error(`Failed to synchronize firewall rules: ${s instanceof Error?s.message:String(s)}`,{cause:s})}}async getChanges(t){let n=_e.safeParse(t);if(!n.success)throw new Error(`Invalid firewall configuration: ${n.error.message}`);try{r.debug("Fetching existing Fastly configuration");let[o,s,a]=await Promise.all([this.client.fetchRules(),this.client.findList(ft),this.client.findList(gt)]),l=this.translateFastlyRules(o),{toAdd:c,toUpdate:d,toDelete:u}=this.diffRules(n.data.rules,l),p=s?.entries??[],f=a?.entries??[],y=[...A.fastlyListEntriesToUnified(p,"deny"),...A.fastlyListEntriesToUnified(f,"allow")],{ipsToAdd:w,ipsToDelete:C}=this.diffIPs(n.data.ips||[],y);return{rulesToAdd:c,rulesToUpdate:d,rulesToDelete:u,ipsToAdd:w,ipsToUpdate:[],ipsToDelete:C,hasChanges:c.length>0||d.length>0||u.length>0||w.length>0||C.length>0}}catch(o){throw r.error("Error fetching existing Fastly configuration:",o),new Error("Failed to fetch existing Fastly configuration")}}getSupportedFeatures(){return{supportsCustomRules:!0,supportsIPBlocking:!0,supportsRateLimiting:!0,supportsManagedRules:!1,supportsGeoBlocking:!0,supportsRedirect:!0,supportsChallenge:!0}}async verifyCredentials(){return this.client.verifyCredentials()}validateConfig(t){let n=super.validateConfig(t),o=[...n.errors],s=[...n.warnings];return t.provider&&t.provider!=="fastly"&&o.push({path:"provider",message:"Provider must be 'fastly' for FastlyFirewallService",code:"INVALID_PROVIDER"}),{valid:o.length===0,errors:o,warnings:s}}getHealthScore(t){let n=super.getHealthScore(t),o=[...n.issues],s=n.score;return t.rules.filter(l=>l.action?.type==="rate_limit").length===0&&(s-=10,o.push({severity:"info",category:"security",message:"No rate limiting rules configured",suggestion:"Consider adding rate limiting to protect against abuse"})),(!t.ips||t.ips.length===0)&&o.push({severity:"info",category:"security",message:"No IP blocking rules configured",suggestion:"Consider blocking known malicious IPs"}),{score:Math.max(0,s),grade:n.grade,issues:o,recommendations:n.recommendations}}diffRules(t,n){let o=[],s=[],a=[...n];for(let l of t){let c=n.find(d=>d.id===l.id);if(!c)o.push(l);else{Le(pe(l),pe(c))||s.push({...l,id:c.id});let d=a.findIndex(u=>u.id===c.id);d!==-1&&a.splice(d,1)}}return{toAdd:o,toUpdate:s,toDelete:a}}diffIPs(t,n){let o=l=>`${l.action}:${l.ip}`,s=new Set(n.map(o)),a=new Set(t.map(o));return{ipsToAdd:t.filter(l=>!s.has(o(l))),ipsToDelete:n.filter(l=>!a.has(o(l)))}}};T();var mt=class{static fromEnv(){let e=process.env.FASTLY_API_TOKEN,t=process.env.FASTLY_WORKSPACE_ID;if(!e)throw new Error("FASTLY_API_TOKEN environment variable is required");if(!t)throw new Error("FASTLY_WORKSPACE_ID environment variable is required");let n=new Ye(t,e);return new Je(n)}static fromConfig(e){let t=e.apiToken||process.env.FASTLY_API_TOKEN,n=e.workspaceId||process.env.FASTLY_WORKSPACE_ID;if(!t)throw new Error("Fastly API token is required (provide apiToken or set FASTLY_API_TOKEN env var)");if(!n)throw new Error("Fastly workspace ID is required (provide workspaceId or set FASTLY_WORKSPACE_ID env var)");r.debug("Creating Fastly provider with config:",{workspaceId:n});let o=new Ye(n,t);return new Je(o)}static create(e,t){return this.fromConfig({workspaceId:e,apiToken:t})}};T();import{GoogleAuth as Jr}from"google-auth-library";var Dn=process.env.DOORMAN_GCP_API_BASE_URL||"https://compute.googleapis.com/compute/v1",Qr="https://www.googleapis.com/auth/compute",Xr=1e3,xn=6e4,Qe=class extends Te{auth;projectId;policyName;constructor(e,t,n){super(`${Dn}/projects/${e}/global`,"gcp"),this.projectId=e,this.policyName=t,this.auth=new Jr({scopes:[Qr],...n?{keyFile:n}:{}})}async getAuthHeaders(){let t=await(await this.auth.getClient()).getRequestHeaders();return Object.fromEntries(t.entries())}policyPath(...e){return["/securityPolicies",this.policyName,...e].join("/")}async getPolicy(){return this.get(this.policyPath())}async addRule(e){let t=await this.post(this.policyPath("addRule"),e);await this.waitForOperation(t)}async patchRule(e,t){let n=await this.post(`${this.policyPath("patchRule")}?priority=${e}`,t);await this.waitForOperation(n)}async removeRule(e){let t=await this.post(`${this.policyPath("removeRule")}?priority=${e}`);await this.waitForOperation(t)}async waitForOperation(e){let t=Date.now()+xn,n=e;for(;n.status!=="DONE";){if(Date.now()>=t)throw new Error(`gcp operation "${n.name}" did not complete within ${xn}ms (last status: ${n.status})`);await this.delay(Xr),n=await this.get(`/operations/${n.name}`)}if(n.httpErrorStatusCode||n.error){let o=n.error?.errors?.map(s=>s.message).join(", ")||n.httpErrorMessage;throw new Error(`gcp operation "${n.name}" failed: ${o||"unknown error"}`)}n.warnings?.length&&n.warnings.forEach(o=>r.warn(`gcp operation "${n.name}" warning: ${o.message}`))}async verifyCredentials(){try{return await this.getPolicy(),!0}catch(e){return r.debug("Credential verification failed:",e),!1}}};T();function kt(i){return i!==void 0&&/^\d+$/.test(i)?Number(i):void 0}var Xe=class extends $e{constructor(t){super();this.client=t}client;name="gcp";async fetchConfig(){try{r.debug("Fetching Cloud Armor security policy");let t=await this.client.getPolicy(),{rules:n,ips:o}=this.translatePolicy(t);return{version:"2.0",provider:"gcp",providers:{gcp:{projectId:this.client.projectId,policyName:this.client.policyName}},rules:n,ips:o}}catch(t){throw r.error("Error fetching Cloud Armor configuration:",t),new Error("Failed to fetch Cloud Armor security policy")}}translatePolicy(t,n){let o=[],s=[];for(let a of t.rules){let l=Ge(a.match.expr.expression);if(!(n?.has(String(a.priority))??!1)&&A.gcpLooksLikeIpRule(a,l)){s.push(A.gcpToUnifiedIP(a,l));continue}let d=A.gcpToUnified(a,l);d.warnings.length>0&&d.warnings.forEach(u=>{let{TranslationWarningSystem:p}=(de(),Re(Oe));r.warn(`Rule ${a.description||a.priority}:
|
|
39
|
+
${p.formatWarning(u)}`)}),o.push(d.result)}return{rules:o,ips:s}}async syncRules(t,n={}){let{dryRun:o=!1}=n;try{let{OperationSafety:s}=(ct(),Re(lt)),a=await s.performDryRunValidation(t,"sync rules",async P=>this.getChanges(P));if(!a.valid)throw new Error(`Dry-run validation failed: ${a.issues.join(", ")}`);let l=a.changes,{rulesToAdd:c,rulesToUpdate:d,rulesToDelete:u}=l,p=l.ipsToAdd||[],f=l.ipsToUpdate||[],y=l.ipsToDelete||[];if(o)return r.info("Dry run mode. The following changes would be made:"),r.info(`Custom Rules - Add: ${c.length}, Update: ${d.length}, Delete: ${u.length}`),r.info(`IP Rules - Add: ${p.length}, Update: ${f.length}, Delete: ${y.length}`),{success:!0,rulesAdded:0,rulesUpdated:0,rulesDeleted:0,ipsAdded:0,ipsUpdated:0,ipsDeleted:0,warnings:a.warnings};let w=s.assessOperationRisk(l,t);if(!await s.confirmDestructiveOperation({operation:"sync rules",target:`Cloud Armor policy ${this.client.policyName}`,changes:l,riskLevel:w,skipConfirmation:n.force||!1,dryRun:!1,allowDeletions:n.allowDeletions||!1}))throw new Error("Operation cancelled by user");let x=[],O=[],F=(P,m)=>`${P}: ${m instanceof Error?m.message:String(m)}`,se=await this.client.getPolicy(),ae=new Set(se.rules.map(P=>P.priority)),fe=1e3,k=()=>{for(;ae.has(fe);)fe+=1e3;return ae.add(fe),fe},le=[];for(let P of[...u,...y]){let m=kt(P.id);if(m!==void 0)try{r.debug(`Removing Cloud Armor rule at priority ${m}`),await this.client.removeRule(m)}catch($){r.error(`Failed to remove Cloud Armor rule at priority ${m}:`,$),O.push(F(`Failed to delete rule at priority ${m}`,$))}}let ge=0;for(let P of c){let m=P.priority??k(),$=A.unifiedToGcp({...P,priority:m});$.warnings.forEach(D=>x.push(D.message));try{r.debug(`Adding Cloud Armor rule at priority ${m}: ${$.result.description}`),await this.client.addRule($.result),ge++,le.push({oldId:P.id,newId:String(m),name:P.name})}catch(D){r.error(`Failed to add Cloud Armor rule "${P.name}":`,D),O.push(F(`Failed to add rule "${P.name}"`,D))}}let me=0;for(let P of d){let m=kt(P.id);if(m===void 0)continue;let $=A.unifiedToGcp(P);$.warnings.forEach(D=>x.push(D.message));try{r.debug(`Updating Cloud Armor rule at priority ${m}`),await this.client.patchRule(m,$.result),me++}catch(D){r.error(`Failed to update Cloud Armor rule at priority ${m}:`,D),O.push(F(`Failed to update rule at priority ${m}`,D))}}let Ie=0;for(let P of p){let m=kt(P.id)??k(),$=A.unifiedIPToGcp(P,m);try{r.debug(`Adding Cloud Armor IP rule at priority ${m}: ${P.ip}`),await this.client.addRule($),Ie++,le.push({oldId:P.id,newId:String(m),name:P.ip})}catch(D){r.error(`Failed to add Cloud Armor IP rule for ${P.ip}:`,D),O.push(F(`Failed to add IP rule for ${P.ip}`,D))}}let we=0;for(let P of f){let m=kt(P.id);if(m!==void 0)try{r.debug(`Updating Cloud Armor IP rule at priority ${m}: ${P.ip}`),await this.client.patchRule(m,A.unifiedIPToGcp(P,m)),we++}catch($){r.error(`Failed to update Cloud Armor IP rule at priority ${m}:`,$),O.push(F(`Failed to update IP rule at priority ${m}`,$))}}return r.debug(`Custom Rules: Added: ${ge}, Updated: ${me}, Deleted: ${u.length}`),r.debug(`IP Rules: Added: ${Ie}, Updated: ${we}, Deleted: ${y.length}`),{success:O.length===0,rulesAdded:ge,rulesUpdated:me,rulesDeleted:u.length,ipsAdded:Ie,ipsUpdated:we,ipsDeleted:y.length,idRemappings:le.length>0?le:void 0,errors:O.length>0?O:void 0,warnings:x.length>0?x:void 0}}catch(s){throw r.error("Error during sync:",s),new Error(`Failed to synchronize firewall rules: ${s instanceof Error?s.message:String(s)}`,{cause:s})}}async getChanges(t){let n=_e.safeParse(t);if(!n.success)throw new Error(`Invalid firewall configuration: ${n.error.message}`);try{r.debug("Fetching existing Cloud Armor configuration");let o=await this.client.getPolicy(),s=new Set(t.rules.map(w=>w.id).filter(w=>w!==void 0)),{rules:a,ips:l}=this.translatePolicy(o,s),{toAdd:c,toUpdate:d,toDelete:u}=this.diffItems(t.rules,a,(w,C)=>Le(pe(w),pe(C))),{toAdd:p,toUpdate:f,toDelete:y}=this.diffItems(t.ips||[],l,(w,C)=>Le(pe(w),pe(C)));return{rulesToAdd:c,rulesToUpdate:d,rulesToDelete:u,ipsToAdd:p,ipsToUpdate:f,ipsToDelete:y,hasChanges:c.length>0||d.length>0||u.length>0||p.length>0||f.length>0||y.length>0}}catch(o){throw r.error("Error fetching existing Cloud Armor configuration:",o),new Error("Failed to fetch existing Cloud Armor configuration")}}getSupportedFeatures(){return{supportsCustomRules:!0,supportsIPBlocking:!0,supportsRateLimiting:!0,supportsManagedRules:!1,supportsGeoBlocking:!0,supportsRedirect:!0,supportsChallenge:!1}}async verifyCredentials(){return this.client.verifyCredentials()}validateConfig(t){let n=super.validateConfig(t),o=[...n.errors],s=[...n.warnings];return t.provider&&t.provider!=="gcp"&&o.push({path:"provider",message:"Provider must be 'gcp' for CloudArmorFirewallService",code:"INVALID_PROVIDER"}),{valid:o.length===0,errors:o,warnings:s}}getHealthScore(t){let n=super.getHealthScore(t),o=[...n.issues],s=n.score;return t.rules.filter(l=>l.action?.type==="rate_limit").length===0&&(s-=10,o.push({severity:"info",category:"security",message:"No rate limiting rules configured",suggestion:"Consider adding rate limiting to protect against abuse"})),(!t.ips||t.ips.length===0)&&o.push({severity:"info",category:"security",message:"No IP blocking rules configured",suggestion:"Consider blocking known malicious IPs"}),{score:Math.max(0,s),grade:n.grade,issues:o,recommendations:n.recommendations}}};T();var ht=class{static fromEnv(){let e=process.env.GOOGLE_CLOUD_PROJECT,t=process.env.GCP_POLICY_NAME,n=process.env.GOOGLE_APPLICATION_CREDENTIALS;if(!e)throw new Error("GOOGLE_CLOUD_PROJECT environment variable is required");if(!t)throw new Error("GCP_POLICY_NAME environment variable is required");let o=new Qe(e,t,n);return new Xe(o)}static fromConfig(e){let t=e.projectId||process.env.GOOGLE_CLOUD_PROJECT,n=e.policyName||process.env.GCP_POLICY_NAME,o=e.serviceAccountKeyPath||process.env.GOOGLE_APPLICATION_CREDENTIALS;if(!t)throw new Error("GCP project ID is required (provide projectId or set GOOGLE_CLOUD_PROJECT env var)");if(!n)throw new Error("Cloud Armor policy name is required (provide policyName or set GCP_POLICY_NAME env var)");r.debug("Creating Cloud Armor provider with config:",{projectId:t,policyName:n});let s=new Qe(t,n,o);return new Xe(s)}static create(e,t,n){return this.fromConfig({projectId:e,policyName:t,serviceAccountKeyPath:n})}};async function On(i){let e;if(i.provider)e=i.provider,r.debug(`Using explicitly specified provider: ${e}`);else{let t=bt.detect(i.config);t.provider?(e=t.provider,r.debug(`Auto-detected provider: ${e} (${t.confidence} confidence)`),t.reasons.length>0&&r.debug(`Reasons: ${t.reasons.join(", ")}`)):i.interactive!==!1?e=await rs():(e="vercel",r.warn("No provider detected, defaulting to Vercel"))}try{let t=await is(e,i);return os(e,t)}catch(t){throw r.error(`Failed to initialize ${e} provider:`,t),t}}function ts(i,e,t){if(!t.configKey)return;let o=i?.providers?.[e]?.[t.configKey];if(typeof o=="string")return o;if(t.legacyConfigKey){let s=i?.[t.legacyConfigKey];if(typeof s=="string")return s}}async function is(i,e){let t=We[i],n=e.config,o={};for(let l of t.fields)o[l.key]=ts(n,i,l);let s=Ki(t,{explicit:e.credentials,config:o}),a=Hi(t,s);if(a.length===0)return s;if(e.interactive===!1){let l=a.map(c=>`--${c.key}`).join(", ");throw new Error(`${yt(i)} credentials missing. Provide ${l}.`)}r.warn(`Missing ${yt(i)} credentials`),r.info(`Please provide your ${yt(i)} credentials:`);for(let l of t.fields){if(s[l.key])continue;let c=l.promptMessage??ns(l),d=l.secret?await Wi(c):await b(c,{type:"text"});s[l.key]=d||void 0}return s}function ns(i){let e=i.required?"":" (optional)",t=i.secret?" ":"";return`${i.label}${e}:${t}`}function os(i,e){switch(i){case"vercel":{let t={token:e.token,projectId:e.projectId,teamId:e.teamId};return{provider:dt.fromConfig(t),vercelCredentials:t}}case"cloudflare":return{provider:pt.fromConfig({apiToken:e.apiToken,zoneId:e.zoneId,accountId:e.accountId})};case"fastly":return{provider:mt.fromConfig({apiToken:e.apiToken,workspaceId:e.workspaceId})};case"gcp":return{provider:ht.fromConfig({projectId:e.projectId,policyName:e.policyName,serviceAccountKeyPath:e.serviceAccountKeyPath})};default:throw new Error(`Unknown provider: ${i}`)}}async function rs(){r.info(es.yellow("Unable to auto-detect firewall provider.")),r.info(`Please select a provider:
|
|
40
40
|
`),j.forEach((s,a)=>{r.info(` ${a+1}. ${yt(s)}`)}),r.info("");let i=await b(`Select provider (1-${j.length}):`,{type:"text"}),e=String(i).trim().toLowerCase(),t=j.find(s=>s===e);if(t)return t;let n=Number(e);if(Number.isInteger(n)&&n>=1&&n<=j.length)return j[n-1];let o=j[0];return r.warn(`Invalid choice: ${i}, defaulting to ${yt(o)}`),o}function yt(i){switch(i){case"vercel":return"Vercel Firewall";case"cloudflare":return"Cloudflare WAF";case"fastly":return"Fastly Next-Gen WAF";case"gcp":return"Google Cloud Armor";default:return i}}function ss(i){if(i.length===1){let{field:t}=i[0],n=t.required?"":" (optional)",o=t.configKey?" or config file":"";return`${t.label}${n} (defaults to ${t.envVar} env var${o})`}return`Resolved against whichever provider is active: ${i.map(t=>`${t.field.label} (${t.field.envVar})`).join(" / ")}`}var ii=new Map;for(let i of j)for(let e of We[i].fields){let t=ii.get(e.key)??[];t.push({provider:i,field:e}),ii.set(e.key,t)}var ee=Object.fromEntries(Array.from(ii.entries()).map(([i,e])=>{let t=e.map(n=>n.field.cliAlias).find(n=>!!n);return[i,{type:"string",...t?{alias:t}:{},description:ss(e)}]}));function K(i){return{token:i.token,projectId:i.projectId,teamId:i.teamId,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,policyName:i.policyName,serviceAccountKeyPath:i.serviceAccountKeyPath}}async function te(i,e){try{i.debug&&(r.level=as.debug);let t;if(i.optionalConfig)try{t=await z(i.config,"optional")}catch{t={}}else i.skipValidation?t=await z(i.config,"raw"):t=await z(i.config,"required");let{provider:n,vercelCredentials:o}=await On({provider:i.provider,config:t,interactive:!i.ci,credentials:K(i)}),s=o?.token??"",a=o?.projectId??"",l=o?.teamId??"";await e({config:t,provider:n,token:s,projectId:a,teamId:l})}catch(t){Q(t,i.errorContext)}}var fs="backup",gs="Backup or restore firewall configurations",ms={config:{alias:"c",type:"string",description:"Path to firewall config file (defaults to .doorman.json)"},provider:X,...ee,output:{alias:"o",type:"string",description:"Output directory for backups",default:"./backups"},restore:{alias:"r",type:"string",description:"Restore from backup file"},list:{alias:"l",type:"boolean",description:"List available backups",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},hs=async i=>{try{i.debug&&(r.level=ps.debug);let e=i.output||"./backups";if(i.list){if(!St(e)){r.info(ie.yellow("No backup directory found."));return}let t=cs(e).filter(n=>n.endsWith(".json")).map(n=>{let o=ni(e,n),s=ds(o);return{name:n,size:s.size,created:s.mtime}}).sort((n,o)=>o.created.getTime()-n.created.getTime());if(t.length===0){r.info(ie.yellow("No backups found."));return}r.log(ie.bold(`
|
|
41
41
|
\u{1F4E6} Available Backups:
|
|
42
42
|
`)),t.forEach(n=>{r.log(`${ie.cyan(n.name)}`),r.log(` ${ie.dim("Created:")} ${n.created.toLocaleString()}`),r.log(` ${ie.dim("Size:")} ${(n.size/1024).toFixed(1)} KB`),r.log("")});return}if(i.restore){let t=i.restore.startsWith("/")?i.restore:ni(e,i.restore);St(t)||(r.error(`Backup file not found: ${t}`),process.exit(1));let n=await z(t,"raw"),o=i.config||".doorman.json";if(St(o)&&!await b(`Config file ${o} already exists. Do you want to overwrite it?`,{type:"confirm"})){r.info(ie.yellow("Restore cancelled."));return}let{backup:s,...a}=n;await G(a,o),r.success(ie.green(`\u2705 Restored configuration from ${t} to ${o}`));return}await te({config:i.config,provider:i.provider,...K(i),debug:i.debug,ci:i.ci,errorContext:"creating backup"},async({provider:t,projectId:n,teamId:o})=>{r.start("Fetching current remote configuration...");let s=await t.fetchConfig(),a=s.metadata?.version??s.version;Ue.getInstance().validateConfig(s),St(e)||ls(e,{recursive:!0});let c=new Date,d=c.toISOString().split("T")[0]??"unknown-date",u=(c.toISOString().split("T")[1]??"").split(".")[0]?.replace(/:/g,"-")??"unknown-time",f=`firewall-backup-${`${d}_${u}`}.json`,y=ni(e,f),w={...s,backup:{createdAt:new Date().toISOString(),source:"remote",provider:t.name,...t.name==="vercel"?{projectId:n,teamId:o}:{},originalVersion:a}};await G(w,y,{validate:!1}),r.success(ie.green(`\u2705 Backup created: ${y}`)),r.log(""),r.log(ie.bold("Backup Details:")),r.log(`${ie.dim("Version:")} ${a??"unknown"}`),r.log(`${ie.dim("Rules:")} ${s.rules.length} custom, ${s.ips?.length??0} IP blocking`),r.log(`${ie.dim("Created:")} ${new Date().toLocaleString()}`),!us(e)&&Bi()&&!qi(e)&&r.warn(ie.yellow(`\u{1F4A1} ${e} isn't in .gitignore \u2014 firewall backups may contain rule details you don't want in git history.`)),r.log(""),r.log(ie.dim("To restore this backup later, run:")),r.log(ie.cyan(`doorman backup --restore ${f}`))})}catch(e){Q(e,"managing backup")}};var ri={};J(ri,{builder:()=>Rs,command:()=>ys,desc:()=>ws,handler:()=>Cs});T();import ne from"chalk";function ye(i){if(ke(i))return i;let e=i,t=e.rules.map(o=>A.vercelToUnified(o).result),n=(e.ips??[]).map(o=>A.vercelIPToUnified(o));return{version:"2.0",provider:"vercel",providers:{vercel:{projectId:e.projectId,teamId:e.teamId}},rules:t,ips:n,metadata:{version:e.version,updatedAt:e.updatedAt}}}function Ut(i,e){let t=!1,n=ke(i),o=n?{...i,metadata:{...i.metadata}}:{...i};if(e.version!==void 0)if(n){let s=o;s.metadata?.version!==e.version&&(s.metadata={...s.metadata,version:e.version,updatedAt:e.updatedAt??new Date().toISOString()},t=!0)}else{let s=o;s.version!==e.version&&(s.version=e.version,s.updatedAt=e.updatedAt??new Date().toISOString(),t=!0)}if(e.idRemappings&&e.idRemappings.length>0){let s=new Map(e.idRemappings.filter(d=>d.oldId).map(d=>[d.oldId,d.newId])),a=new Map(e.idRemappings.filter(d=>!d.oldId).map(d=>[d.name,d.newId])),l=!1,c=o.rules.map(d=>{let u=d.id?s.get(d.id):a.get(d.name);return u&&u!==d.id?(l=!0,{...d,id:u}):d});l&&(o={...o,rules:c},t=!0)}return{config:o,changed:t}}function Fn(i,e){if(ke(e))return{...e,...i};let t=e,n=i.rules.map(s=>A.unifiedToVercel(s).result),o=(i.ips??[]).map(s=>({id:s.id||"",ip:s.ip,hostname:s.hostname||"",action:s.action,notes:s.notes}));return{...t,projectId:i.providers?.vercel?.projectId??t.projectId,teamId:i.providers?.vercel?.teamId??t.teamId,version:i.metadata?.version,updatedAt:i.metadata?.updatedAt,rules:n,ips:o}}var ys="diff",ws="Show detailed differences between local and remote firewall configuration",Rs={config:{alias:"c",type:"string",description:"Path to firewall config file"},provider:X,...ee,format:{alias:"f",type:"string",choices:["table","json"],description:"Output format",default:"table"},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},Cs=async i=>{await te({config:i.config,provider:i.provider,...K(i),debug:i.debug,ci:i.ci,errorContext:"calculating diff"},async({config:e,provider:t})=>{await bs(t,ye(e),i)})};async function bs(i,e,t){let n=t.format==="json";n||r.start("Calculating differences...");let o=await i.getChanges(e),s=o.ipsToAdd??[],a=o.ipsToUpdate??[],l=o.ipsToDelete??[];if(n){r.log(JSON.stringify({rules:{toAdd:o.rulesToAdd,toUpdate:o.rulesToUpdate,toDelete:o.rulesToDelete},ips:{toAdd:s,toUpdate:a,toDelete:l},summary:{hasChanges:o.hasChanges,ruleChanges:o.rulesToAdd.length+o.rulesToUpdate.length+o.rulesToDelete.length,ipChanges:s.length+a.length+l.length}},null,2));return}if(!o.hasChanges){r.success(ne.green("No differences found. Local and remote configurations are in sync."));return}r.log(ne.bold(`
|