@gfargo/doorman 3.13.5 → 3.13.6
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 +1 -1
- package/dist/bin/run.mjs +1 -1
- package/package.json +1 -1
package/dist/bin/run.js
CHANGED
|
@@ -34,7 +34,7 @@ ${V}`)}),C.result}),{toAdd:l,toUpdate:c,toDelete:d}=this.diffRules(ft(o.data.rul
|
|
|
34
34
|
\u2022 Find your Account ID in the Cloudflare dashboard sidebar`)):n.includes("permission")||n.includes("forbidden")?(r.warn(`\u{1F6AB} Lists API permission denied: Insufficient permissions for ${t}. Falling back to individual IP rules.`),r.info(`\u{1F4A1} To fix Lists API permissions:
|
|
35
35
|
\u2022 Ensure your API token has Account:Read permissions
|
|
36
36
|
\u2022 Verify the account ID is correct
|
|
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 wt=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 yt(o,s,a)}static fromEnv(){return this.create()}static fromConfig(e){return this.create(e.apiToken,e.zoneId,e.accountId)}};T();T();var Gn=process.env.DOORMAN_FASTLY_API_BASE_URL||"https://api.fastly.com",Rt="doorman-managed-deny",Ct="doorman-managed-allow",tt=class extends _e{constructor(t,n){super(Gn,"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)}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})}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 it=class extends ke{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(Rt),this.client.findList(Ct)]),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}=(pe(),Ie(Be));r.warn(`Rule ${o.description||o.id}:
|
|
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 wt=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 yt(o,s,a)}static fromEnv(){return this.create()}static fromConfig(e){return this.create(e.apiToken,e.zoneId,e.accountId)}};T();T();var Gn=process.env.DOORMAN_FASTLY_API_BASE_URL||"https://api.fastly.com",Rt="doorman-managed-deny",Ct="doorman-managed-allow",tt=class extends _e{constructor(t,n){super(Gn,"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 it=class extends ke{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(Rt),this.client.findList(Ct)]),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}=(pe(),Ie(Be));r.warn(`Rule ${o.description||o.id}:
|
|
38
38
|
${l.formatWarning(a)}`)}),n.push(s.result)}return n}async syncRules(t,n={}){let{dryRun:o=!1}=n;try{let{OperationSafety:s}=(mt(),Ie(gt)),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=[];Ot(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)})),N=d.map(m=>({rule:m,translated:A.unifiedToFastly(m)})),V=[],ce=(m,$)=>`${m}: ${$ instanceof Error?$.message:String($)}`,de=[],me=[],S=[],ue=[];for(let m of u)if(m.id)try{r.debug(`Deleting Fastly rule: ${m.id}`),await this.client.deleteRule(m.id),S.push(m.id)}catch($){r.error(`Failed to delete Fastly rule ${m.id}:`,$),V.push(ce(`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 we=await this.client.createRule(D);de.push(we),ue.push({oldId:m.id||void 0,newId:we.id,name:D.description})}catch(we){r.error(`Failed to add Fastly rule "${D.description}":`,we),V.push(ce(`Failed to add rule "${D.description}"`,we))}}for(let{rule:m,translated:$}of N)if(m.id)try{r.debug(`Updating Fastly rule: ${m.id}`);let D=await this.client.updateRule(m.id,$.result);me.push(D)}catch(D){r.error(`Failed to update Fastly rule ${m.id}:`,D),V.push(ce(`Failed to update rule ${m.id}`,D))}let he=0,ye=0,Te=t.ips||[],{result:ve,warnings:P}=A.unifiedIPsToFastlyEntries(Te);P.forEach(m=>C.push(m.message));try{for(let[m,$]of[[Rt,ve.denyEntries],[Ct,ve.allowEntries]]){let we=(await this.client.findList(m))?.entries??[],st=$.filter(g=>!we.includes(g)),at=we.filter(g=>!$.includes(g));if(st.length===0&&at.length===0)continue;let Ne=await this.client.getOrCreateList(m);await this.client.updateListEntries(Ne.id,$),he+=st.length,ye+=at.length}}catch(m){r.error("Failed to sync Fastly IP lists:",m),V.push(ce("Failed to sync IP lists",m))}return r.debug(`Custom Rules: Added: ${de.length}, Updated: ${me.length}, Deleted: ${S.length}`),r.debug(`IP Rules: Added: ${he}, Deleted: ${ye}`),{success:V.length===0,rulesAdded:de.length,rulesUpdated:me.length,rulesDeleted:S.length,ipsAdded:he,ipsUpdated:0,ipsDeleted:ye,idRemappings:ue.length>0?ue:void 0,errors:V.length>0?V: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=Ue.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(Rt),this.client.findList(Ct)]),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:R,ipsToDelete:C}=this.diffIPs(n.data.ips||[],y);return{rulesToAdd:c,rulesToUpdate:d,rulesToDelete:u,ipsToAdd:R,ipsToUpdate:[],ipsToDelete:C,hasChanges:c.length>0||d.length>0||u.length>0||R.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{Oe(ge(l),ge(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 bt=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 tt(t,e);return new it(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 tt(n,t);return new it(o)}static create(e,t){return this.fromConfig({workspaceId:e,apiToken:t})}};var Hn=require("google-auth-library");T();var Zn=process.env.DOORMAN_GCP_API_BASE_URL||"https://compute.googleapis.com/compute/v1",rs="https://www.googleapis.com/auth/compute",ss=1e3,Kn=6e4,nt=class extends _e{auth;projectId;policyName;constructor(e,t,n){super(`${Zn}/projects/${e}/global`,"gcp"),this.projectId=e,this.policyName=t,this.auth=new Hn.GoogleAuth({scopes:[rs],...n?{keyFile:n}:{}})}async getAuthHeaders(){return await(await this.auth.getClient()).getRequestHeaders()}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()+Kn,n=e;for(;n.status!=="DONE";){if(Date.now()>=t)throw new Error(`gcp operation "${n.name}" did not complete within ${Kn}ms (last status: ${n.status})`);await this.delay(ss),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 Vt(i){return i!==void 0&&/^\d+$/.test(i)?Number(i):void 0}var ot=class extends ke{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 n=[],o=[];for(let s of t.rules){if(A.gcpLooksLikeIpRule(s)){o.push(A.gcpToUnifiedIP(s));continue}let a=A.gcpToUnified(s);a.warnings.length>0&&a.warnings.forEach(l=>{let{TranslationWarningSystem:c}=(pe(),Ie(Be));r.warn(`Rule ${s.description||s.priority}:
|
|
39
39
|
${c.formatWarning(l)}`)}),n.push(a.result)}return{rules:n,ips:o}}async syncRules(t,n={}){let{dryRun:o=!1}=n;try{let{OperationSafety:s}=(mt(),Ie(gt)),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 R=s.assessOperationRisk(l,t);if(!await s.confirmDestructiveOperation({operation:"sync rules",target:`Cloud Armor policy ${this.client.policyName}`,changes:l,riskLevel:R,skipConfirmation:n.force||!1,dryRun:!1,allowDeletions:n.allowDeletions||!1}))throw new Error("Operation cancelled by user");let x=[],N=[],V=(P,m)=>`${P}: ${m instanceof Error?m.message:String(m)}`,ce=await this.client.getPolicy(),de=new Set(ce.rules.map(P=>P.priority)),me=1e3,S=()=>{for(;de.has(me);)me+=1e3;return de.add(me),me},ue=[];for(let P of[...u,...y]){let m=Vt(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}:`,$),N.push(V(`Failed to delete rule at priority ${m}`,$))}}let he=0;for(let P of c){let m=P.priority??S(),$=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),he++,ue.push({oldId:P.id,newId:String(m),name:P.name})}catch(D){r.error(`Failed to add Cloud Armor rule "${P.name}":`,D),N.push(V(`Failed to add rule "${P.name}"`,D))}}let ye=0;for(let P of d){let m=Vt(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),ye++}catch(D){r.error(`Failed to update Cloud Armor rule at priority ${m}:`,D),N.push(V(`Failed to update rule at priority ${m}`,D))}}let Te=0;for(let P of p){let m=Vt(P.id)??S(),$=A.unifiedIPToGcp(P,m);try{r.debug(`Adding Cloud Armor IP rule at priority ${m}: ${P.ip}`),await this.client.addRule($),Te++,ue.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),N.push(V(`Failed to add IP rule for ${P.ip}`,D))}}let ve=0;for(let P of f){let m=Vt(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)),ve++}catch($){r.error(`Failed to update Cloud Armor IP rule at priority ${m}:`,$),N.push(V(`Failed to update IP rule at priority ${m}`,$))}}return r.debug(`Custom Rules: Added: ${he}, Updated: ${ye}, Deleted: ${u.length}`),r.debug(`IP Rules: Added: ${Te}, Updated: ${ve}, Deleted: ${y.length}`),{success:N.length===0,rulesAdded:he,rulesUpdated:ye,rulesDeleted:u.length,ipsAdded:Te,ipsUpdated:ve,ipsDeleted:y.length,idRemappings:ue.length>0?ue:void 0,errors:N.length>0?N: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=Ue.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(),{rules:s,ips:a}=this.translatePolicy(o),{toAdd:l,toUpdate:c,toDelete:d}=this.diffItems(t.rules,s,(y,R)=>Oe(ge(y),ge(R))),{toAdd:u,toUpdate:p,toDelete:f}=this.diffItems(t.ips||[],a,(y,R)=>Oe(ge(y),ge(R)));return{rulesToAdd:l,rulesToUpdate:c,rulesToDelete:d,ipsToAdd:u,ipsToUpdate:p,ipsToDelete:f,hasChanges:l.length>0||c.length>0||d.length>0||u.length>0||p.length>0||f.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 vt=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 nt(e,t,n);return new ot(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 nt(t,n,o);return new ot(s)}static create(e,t,n){return this.fromConfig({projectId:e,policyName:t,serviceAccountKeyPath:n})}};async function Jn(i){let e;if(i.provider)e=i.provider,r.debug(`Using explicitly specified provider: ${e}`);else{let t=Pt.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 us():(e="vercel",r.warn("No provider detected, defaulting to Vercel"))}try{let t=await ls(e,i);return ds(e,t)}catch(t){throw r.error(`Failed to initialize ${e} provider:`,t),t}}function as(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 ls(i,e){let t=He[i],n=e.config,o={};for(let l of t.fields)o[l.key]=as(n,i,l);let s=cn(t,{explicit:e.credentials,config:o}),a=dn(t,s);if(a.length===0)return s;if(e.interactive===!1){let l=a.map(c=>`--${c.key}`).join(", ");throw new Error(`${It(i)} credentials missing. Provide ${l}.`)}r.warn(`Missing ${It(i)} credentials`),r.info(`Please provide your ${It(i)} credentials:`);for(let l of t.fields){if(s[l.key])continue;let c=l.promptMessage??cs(l),d=l.secret?await on(c):await b(c,{type:"text"});s[l.key]=d||void 0}return s}function cs(i){let e=i.required?"":" (optional)",t=i.secret?" ":"";return`${i.label}${e}:${t}`}function ds(i,e){switch(i){case"vercel":{let t={token:e.token,projectId:e.projectId,teamId:e.teamId};return{provider:ht.fromConfig(t),vercelCredentials:t}}case"cloudflare":return{provider:wt.fromConfig({apiToken:e.apiToken,zoneId:e.zoneId,accountId:e.accountId})};case"fastly":return{provider:bt.fromConfig({apiToken:e.apiToken,workspaceId:e.workspaceId})};case"gcp":return{provider:vt.fromConfig({projectId:e.projectId,policyName:e.policyName,serviceAccountKeyPath:e.serviceAccountKeyPath})};default:throw new Error(`Unknown provider: ${i}`)}}async function us(){r.info(Yn.default.yellow("Unable to auto-detect firewall provider.")),r.info(`Please select a provider:
|
|
40
40
|
`),z.forEach((s,a)=>{r.info(` ${a+1}. ${It(s)}`)}),r.info("");let i=await b(`Select provider (1-${z.length}):`,{type:"text"}),e=String(i).trim().toLowerCase(),t=z.find(s=>s===e);if(t)return t;let n=Number(e);if(Number.isInteger(n)&&n>=1&&n<=z.length)return z[n-1];let o=z[0];return r.warn(`Invalid choice: ${i}, defaulting to ${It(o)}`),o}function It(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 ps(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 ui=new Map;for(let i of z)for(let e of He[i].fields){let t=ui.get(e.key)??[];t.push({provider:i,field:e}),ui.set(e.key,t)}var ie=Object.fromEntries(Array.from(ui.entries()).map(([i,e])=>{let t=e.map(n=>n.field.cliAlias).find(n=>!!n);return[i,{type:"string",...t?{alias:t}:{},description:ps(e)}]}));function Z(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 ne(i,e){try{i.debug&&(r.level=Qn.LogLevels.debug);let t;if(i.optionalConfig)try{t=await G(i.config,"optional")}catch{t={}}else i.skipValidation?t=await G(i.config,"raw"):t=await G(i.config,"required");let{provider:n,vercelCredentials:o}=await Jn({provider:i.provider,config:t,interactive:!i.ci,credentials:Z(i)}),s=o?.token??"",a=o?.projectId??"",l=o?.teamId??"";await e({config:t,provider:n,token:s,projectId:a,teamId:l})}catch(t){ee(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:te,...ie,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=Xn.LogLevels.debug);let e=i.output||"./backups";if(i.list){if(!(0,Ae.existsSync)(e)){r.info(Q.default.yellow("No backup directory found."));return}let t=(0,Ae.readdirSync)(e).filter(n=>n.endsWith(".json")).map(n=>{let o=(0,rt.join)(e,n),s=(0,Ae.statSync)(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(Q.default.yellow("No backups found."));return}r.log(Q.default.bold(`
|
package/dist/bin/run.mjs
CHANGED
|
@@ -34,7 +34,7 @@ ${F}`)}),C.result}),{toAdd:l,toUpdate:c,toDelete:d}=this.diffRules(st(o.data.rul
|
|
|
34
34
|
\u2022 Find your Account ID in the Cloudflare dashboard sidebar`)):n.includes("permission")||n.includes("forbidden")?(r.warn(`\u{1F6AB} Lists API permission denied: Insufficient permissions for ${t}. Falling back to individual IP rules.`),r.info(`\u{1F4A1} To fix Lists API permissions:
|
|
35
35
|
\u2022 Ensure your API token has Account:Read permissions
|
|
36
36
|
\u2022 Verify the account ID is correct
|
|
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 ut=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 dt(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",pt="doorman-managed-deny",ft="doorman-managed-allow",Ze=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)}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})}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 Ye=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(pt),this.client.findList(ft)]),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}:
|
|
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 ut=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 dt(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",pt="doorman-managed-deny",ft="doorman-managed-allow",Ze=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 Ye=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(pt),this.client.findList(ft)]),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
38
|
${l.formatWarning(a)}`)}),n.push(s.result)}return n}async syncRules(t,n={}){let{dryRun:o=!1}=n;try{let{OperationSafety:s}=(lt(),Re(at)),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[[pt,we.denyEntries],[ft,we.allowEntries]]){let he=(await this.client.findList(m))?.entries??[],tt=$.filter(g=>!he.includes(g)),it=he.filter(g=>!$.includes(g));if(tt.length===0&&it.length===0)continue;let xe=await this.client.getOrCreateList(m);await this.client.updateListEntries(xe.id,$),ge+=tt.length,me+=it.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(pt),this.client.findList(ft)]),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 gt=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 Ze(t,e);return new Ye(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 Ze(n,t);return new Ye(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,Je=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(){return await(await this.auth.getClient()).getRequestHeaders()}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 Qe=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 n=[],o=[];for(let s of t.rules){if(A.gcpLooksLikeIpRule(s)){o.push(A.gcpToUnifiedIP(s));continue}let a=A.gcpToUnified(s);a.warnings.length>0&&a.warnings.forEach(l=>{let{TranslationWarningSystem:c}=(de(),Re(Oe));r.warn(`Rule ${s.description||s.priority}:
|
|
39
39
|
${c.formatWarning(l)}`)}),n.push(a.result)}return{rules:n,ips:o}}async syncRules(t,n={}){let{dryRun:o=!1}=n;try{let{OperationSafety:s}=(lt(),Re(at)),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(),{rules:s,ips:a}=this.translatePolicy(o),{toAdd:l,toUpdate:c,toDelete:d}=this.diffItems(t.rules,s,(y,w)=>Le(pe(y),pe(w))),{toAdd:u,toUpdate:p,toDelete:f}=this.diffItems(t.ips||[],a,(y,w)=>Le(pe(y),pe(w)));return{rulesToAdd:l,rulesToUpdate:c,rulesToDelete:d,ipsToAdd:u,ipsToUpdate:p,ipsToDelete:f,hasChanges:l.length>0||c.length>0||d.length>0||u.length>0||p.length>0||f.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 mt=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 Je(e,t,n);return new Qe(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 Je(t,n,o);return new Qe(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=Ct.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(`${ht(i)} credentials missing. Provide ${l}.`)}r.warn(`Missing ${ht(i)} credentials`),r.info(`Please provide your ${ht(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:ct.fromConfig(t),vercelCredentials:t}}case"cloudflare":return{provider:ut.fromConfig({apiToken:e.apiToken,zoneId:e.zoneId,accountId:e.accountId})};case"fastly":return{provider:gt.fromConfig({apiToken:e.apiToken,workspaceId:e.workspaceId})};case"gcp":return{provider:mt.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}. ${ht(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 ${ht(o)}`),o}function ht(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(`
|