supascan 0.0.5 → 0.0.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/README.md CHANGED
@@ -5,7 +5,7 @@ A security analysis CLI tool for Supabase databases that helps identify exposed
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install -g supascan
8
+ bun install -g supascan
9
9
  ```
10
10
 
11
11
  ## Usage
package/dist/supascan.js CHANGED
@@ -603,4 +603,4 @@ document.addEventListener('DOMContentLoaded', function() {
603
603
  .scrollbar-thin::-webkit-scrollbar-thumb:hover {
604
604
  background: #94a3b8;
605
605
  }
606
- `},void 0,!1,void 0,this)]},void 0,!0,void 0,this),$("body",{class:"bg-slate-50 min-h-screen scrollbar-thin m-0 p-0",children:$("div",{class:"w-full min-h-screen px-6 py-6",children:[$("header",{class:"bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6 fade-in",children:$("div",{class:"flex items-center justify-between",children:[$("div",{class:"flex-1",children:[$("h1",{class:"text-2xl font-bold text-slate-900 mb-1 font-mono",children:"Supabase Database Analysis"},void 0,!1,void 0,this),$("p",{class:"text-slate-600 font-mono text-sm",children:["Generated on ",new Date().toLocaleString()]},void 0,!0,void 0,this)]},void 0,!0,void 0,this),$("div",{class:"flex items-center gap-8",children:[$("div",{class:"text-right",children:[$("div",{class:"text-xs text-slate-500 font-mono",children:"Schemas Analyzed"},void 0,!1,void 0,this),$("div",{class:"text-xl font-bold text-emerald-600 font-mono",children:p.schemas.length},void 0,!1,void 0,this)]},void 0,!0,void 0,this),$("button",{onclick:"saveReport()",class:"px-3 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors font-mono text-sm flex items-center gap-2",children:[$("svg",{class:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:$("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3-3m0 0l-3 3m3-3v12"},void 0,!1,void 0,this)},void 0,!1,void 0,this),"Save Report"]},void 0,!0,void 0,this)]},void 0,!0,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this),gm({domain:p.summary.domain,url:d,key:a,metadata:p.summary.metadata,jwtInfo:p.summary.jwtInfo}),$("section",{class:"space-y-8",children:[$("h2",{class:"text-2xl font-bold text-gray-900 mb-6 flex items-center",children:[$("svg",{class:"w-6 h-6 mr-3 text-supabase-green",fill:"currentColor",viewBox:"0 0 20 20",children:$("path",{fillRule:"evenodd",d:"M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z",clipRule:"evenodd"},void 0,!1,void 0,this)},void 0,!1,void 0,this),"Database Analysis"]},void 0,!0,void 0,this),Object.entries(p.schemaDetails).map(([m,D])=>nm({schema:m,analysis:D}))]},void 0,!0,void 0,this),$("footer",{class:"mt-12 text-center text-slate-500 text-sm font-mono",children:$("p",{children:"Generated by supascan - Security analysis tool for Supabase"},void 0,!1,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}}async function N9(p,d){let a=await e4.analyze(p,d.schema);if(!a.success)G.error("Analysis failed",a.error.message),process.exit(1);if(p.json)console.log(JSON.stringify(a.value,null,2));else if(p.html){let m=b4.generateHtmlReport(a.value,p.url,p.key),D=r3();p9(D,m),d9(D),G.success(`HTML report generated: ${D}`)}else vD(a.value)}function vD(p){if(console.log(),console.log(z.default.bold(z.default.cyan("━".repeat(60)))),console.log(z.default.bold(z.default.cyan(" SUPABASE DATABASE ANALYSIS"))),console.log(z.default.bold(z.default.cyan("━".repeat(60)))),console.log(),console.log(z.default.bold(z.default.yellow("TARGET SUMMARY"))),console.log(z.default.dim("─".repeat(20))),console.log(z.default.bold("Domain:"),z.default.white(p.summary.domain)),p.summary.metadata?.service)console.log(z.default.bold("Service:"),z.default.white(p.summary.metadata.service));if(p.summary.metadata?.region)console.log(z.default.bold("Project ID:"),z.default.white(p.summary.metadata.region));if(p.summary.metadata?.title)console.log(z.default.bold("Title:"),z.default.white(p.summary.metadata.title));if(p.summary.metadata?.version)console.log(z.default.bold("Version:"),z.default.white(p.summary.metadata.version));if(p.summary.jwtInfo){if(console.log(),console.log(z.default.bold(z.default.yellow("JWT TOKEN INFO"))),console.log(z.default.dim("─".repeat(20))),p.summary.jwtInfo.iss)console.log(z.default.bold("Issuer:"),z.default.white(p.summary.jwtInfo.iss));if(p.summary.jwtInfo.aud)console.log(z.default.bold("Audience:"),z.default.white(p.summary.jwtInfo.aud));if(p.summary.jwtInfo.role)console.log(z.default.bold("Role:"),z.default.white(p.summary.jwtInfo.role));if(p.summary.jwtInfo.exp){let d=new Date(p.summary.jwtInfo.exp*1000);console.log(z.default.bold("Expires:"),z.default.white(d.toISOString()))}if(p.summary.jwtInfo.iat){let d=new Date(p.summary.jwtInfo.iat*1000);console.log(z.default.bold("Issued:"),z.default.white(d.toISOString()))}}console.log(),console.log(z.default.bold(z.default.cyan("DATABASE ANALYSIS"))),console.log(z.default.dim("─".repeat(20))),console.log(z.default.bold("Schemas discovered:"),z.default.green(p.schemas.length.toString())),console.log(),Object.entries(p.schemaDetails).forEach(([d,a])=>{console.log(z.default.bold(z.default.cyan(`Schema: ${d}`))),console.log();let m=Object.values(a.tableAccess).filter((l)=>l.status==="readable").length,D=Object.values(a.tableAccess).filter((l)=>l.status==="denied").length,v=Object.values(a.tableAccess).filter((l)=>l.status==="empty").length;if(console.log(z.default.bold("Tables:"),z.default.green(a.tables.length.toString())),console.log(z.default.dim(` ${m} exposed • ${v} empty/protected • ${D} denied`)),console.log(),a.tables.length>0)a.tables.forEach((l)=>{let u=a.tableAccess[l],w="",i="";switch(u?.status){case"readable":w=z.default.green("✓"),i=z.default.dim("(data exposed)");break;case"empty":w=z.default.yellow("○"),i=z.default.dim("(0 rows - empty or RLS)");break;case"denied":w=z.default.red("✗"),i=z.default.dim("(access denied)");break}console.log(` ${w} ${z.default.white(l)} ${i}`)});else console.log(z.default.dim(" No tables found"));if(console.log(),console.log(z.default.bold("RPCs:"),z.default.green(a.rpcs.length.toString())),a.rpcFunctions.length>0)a.rpcFunctions.forEach((l)=>{if(console.log(` • ${z.default.white(l.name)}`),l.parameters.length>0)l.parameters.forEach((u)=>{let w=u.required?z.default.red("(required)"):z.default.dim("(optional)"),i=u.format?`${u.type} (${u.format})`:u.type;console.log(` - ${z.default.cyan(u.name)}: ${z.default.yellow(i)} ${w}`)});else console.log(z.default.dim(" No parameters"))});else console.log(z.default.dim(" No RPCs found"));console.log()}),console.log(z.default.bold(z.default.cyan("━".repeat(60)))),console.log()}var j=e2(I6(),1);async function V9(p,d){let a=d.dump.split(".");if(a.length===1&&a[0]){let u=a[0],w=await V1.getSwagger(p,u);if(!w.success)G.error("Failed to get swagger",w.error.message),process.exit(1);if(p.json)console.log(JSON.stringify(w.value,null,2));else uD(u,w.value);return}if(a.length!==2||!a[0]||!a[1])G.error("Invalid format. Use: schema.table or schema"),process.exit(1);let m=a[0],D=a[1],v=parseInt(d.limit),l=await V1.dumpTable(p,m,D,v);if(!l.success)G.error("Failed to dump table",l.error.message),process.exit(1);if(p.json)console.log(JSON.stringify(l.value,null,2));else wD(m,D,l.value)}function uD(p,d){console.log(),console.log(j.default.bold(j.default.cyan("━".repeat(60)))),console.log(j.default.bold(j.default.cyan(` SWAGGER DUMP: ${p}`))),console.log(j.default.bold(j.default.cyan("━".repeat(60)))),console.log(),console.log(JSON.stringify(d,null,2)),console.log(),console.log(j.default.bold(j.default.cyan("━".repeat(60)))),console.log()}function wD(p,d,a){if(console.log(),console.log(j.default.bold(j.default.cyan("━".repeat(60)))),console.log(j.default.bold(j.default.cyan(` TABLE DUMP: ${p}.${d}`))),console.log(j.default.bold(j.default.cyan("━".repeat(60)))),console.log(),console.log(j.default.bold("Total rows:"),j.default.green(a.count.toString())),console.log(j.default.bold("Showing:"),j.default.green(a.rows.length.toString())),console.log(j.default.bold("Columns:"),j.default.green(a.columns.length.toString())),console.log(),console.log(j.default.dim(a.columns.join(", "))),console.log(),a.rows.length>0)console.table(a.rows);else console.log(j.default.dim("No rows found"));console.log(),console.log(j.default.bold(j.default.cyan("━".repeat(60)))),console.log()}var W=e2(I6(),1);async function B9(p,d){let a=d.rpc.split(".");if(a.length!==2||!a[0]||!a[1])G.error("Invalid RPC format. Use: schema.rpc_name"),process.exit(1);let m=a[0],D=a[1],v=await V1.getRPCsWithParameters(p,m),l=null;if(v.success)l=v.value.find((i)=>i.name===`rpc/${D}`)||null;else G.warn("Failed to get RPC functions from schema, proceeding without validation",v.error.message);if(!d.args){iD(m,D,l);return}let u={};try{u=o3(d.args)}catch(i){G.error("Failed to parse RPC arguments",i instanceof Error?i.message:String(i)),process.exit(1)}if(l){let F=l.parameters.filter((C)=>C.required).filter((C)=>!(C.name in u));if(F.length>0)G.error(`Missing required parameters: ${F.map((C)=>C.name).join(", ")}`),process.exit(1)}else G.warn("Skipping parameter validation due to schema introspection failure");let w=await V1.callRPC(p,m,D,u,{get:!0,explain:d.explain,limit:parseInt(d.limit)});if(!w.success)G.error("RPC call failed",w.error.message),process.exit(1);if(p.json)console.log(JSON.stringify(w.value,null,2));else FD(m,D,w.value,d.explain)}function iD(p,d,a){if(console.log(),console.log(W.default.bold(W.default.cyan("━".repeat(60)))),console.log(W.default.bold(W.default.cyan(` RPC HELP: ${p}.${d}`))),console.log(W.default.bold(W.default.cyan("━".repeat(60)))),console.log(),a&&a.parameters.length>0)console.log(W.default.bold("Parameters:")),a.parameters.forEach((m)=>{let D=m.required?W.default.red("(required)"):W.default.dim("(optional)"),v=m.format?`${m.type} (${m.format})`:m.type;if(console.log(` • ${W.default.cyan(m.name)}: ${W.default.yellow(v)} ${D}`),m.description)console.log(W.default.dim(` ${m.description}`))}),console.log(),console.log(W.default.bold("Usage:")),console.log(W.default.dim(`supascan --rpc "${p}.${d}" --args '{"param1": "value1", "param2": "value2"}'`));else if(a)console.log(W.default.dim("No parameters required")),console.log(),console.log(W.default.bold("Usage:")),console.log(W.default.dim(`supascan --rpc "${p}.${d}"`));else console.log(W.default.yellow("⚠️ Schema introspection failed - parameter information unavailable")),console.log(),console.log(W.default.bold("Usage:")),console.log(W.default.dim(`supascan --rpc "${p}.${d}" --args '{"param1": "value1"}'`)),console.log(),console.log(W.default.dim("Note: You can still call the RPC, but parameter validation is disabled"));console.log(),console.log(W.default.bold(W.default.cyan("━".repeat(60)))),console.log()}function FD(p,d,a,m){if(console.log(),console.log(W.default.bold(W.default.cyan("━".repeat(60)))),m)console.log(W.default.bold(W.default.cyan(` QUERY PLAN: ${p}.${d}`)));else console.log(W.default.bold(W.default.cyan(` RPC RESULT: ${p}.${d}`)));if(console.log(W.default.bold(W.default.cyan("━".repeat(60)))),console.log(),m)if(console.log(W.default.bold("Execution Plan:")),console.log(),typeof a==="string")console.log(W.default.yellow(a));else console.log(JSON.stringify(a,null,2));else if(Array.isArray(a))if(console.log(W.default.bold("Results:"),W.default.green(a.length.toString())),console.log(),a.length>0)console.table(a);else console.log(W.default.dim("No results returned"));else if(typeof a==="object"&&a!==null)console.log(W.default.bold("Result:")),console.table([a]);else console.log(W.default.bold("Result:"),W.default.green(String(a)));console.log(),console.log(W.default.bold(W.default.cyan("━".repeat(60)))),console.log()}var P0=e2(Cp(),1);class b0{static URL_PATTERNS=[/https:\/\/[a-z0-9-]+\.supabase\.co\/?/g,/['"`]https:\/\/[a-z0-9-]+\.supabase\.co\/?['"`]/g];static KEY_PATTERNS=[/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,/['"`]eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+['"`]/g];static CREATE_BROWSER_CLIENT_PATTERN=/createBrowserClient\)\s*\(\s*["']([^"']+)["']\s*,\s*["']([^"']+)["']/g;static SCRIPT_SRC_PATTERN=/<script[^>]+src=["']([^"']+)["']/gi;static INLINE_SCRIPT_PATTERN=/<script[^>]*>([\s\S]*?)<\/script>/gi;static async extractFromUrl(p,d){G.debug(d,`Fetching content from: ${p}`);let a=await fetch(p);if(!a.ok)return F1(Error(`Failed to fetch URL: ${a.status} ${a.statusText}`));let m=await a.text(),D=a.headers.get("content-type")??"";G.debug(d,`Fetched ${m.length} bytes (${D})`);let v=D.includes("text/html")||m.trim().startsWith("<!DOCTYPE")||m.trim().startsWith("<html");if(p.endsWith(".js")||D.includes("javascript")||D.includes("ecmascript"))return this.extractFromContent(m,d,p);if(v)return await this.extractFromHtml(m,p,d);return this.extractFromContent(m,d,p)}static async extractFromHtml(p,d,a){G.debug(a,"Detected HTML content, searching for JS files...");let m=Array.from(p.matchAll(this.INLINE_SCRIPT_PATTERN));G.debug(a,`Found ${m.length} inline scripts`);for(let v of m){let l=v[1];if(!l)continue;let u=this.extractFromContent(l,a,"inline script");if(u.success)return G.debug(a,"Found credentials in inline script"),u}let D=Array.from(p.matchAll(this.SCRIPT_SRC_PATTERN));G.debug(a,`Found ${D.length} external scripts`);for(let v of D){let l=v[1];if(!l)continue;let u=this.resolveUrl(l,d);G.debug(a,`Checking script: ${u}`);let w=await fetch(u);if(!w.ok){G.debug(a,`Failed to fetch ${u}`);continue}let i=await w.text(),F=this.extractFromContent(i,a,u);if(F.success)return G.debug(a,`Found credentials in ${u}`),F}return F1(Error("No Supabase credentials found in any scripts"))}static extractFromContent(p,d,a){G.debug(d,"Extracting Supabase credentials...");let m=this.CREATE_BROWSER_CLIENT_PATTERN.exec(p);if(m){let w=m[1],i=m[2];if(w&&i)return G.debug(d,"Found createBrowserClient pattern"),G.debug(d,`Extracted URL: ${w}`),G.debug(d,`Extracted key: ${i.substring(0,20)}...`),u1({url:w,key:i,source:a})}let D=this.findClosestPairs(p);if(G.debug(d,`Found ${D.length} potential URL-key pairs`),D.length===0)return F1(Error("No Supabase URL-key pairs found in content"));let v=D[0];if(!v)return F1(Error("No valid URL-key pairs found"));let{url:l,key:u}=v;return G.debug(d,`Extracted URL: ${l}`),G.debug(d,`Extracted key: ${u.substring(0,20)}...`),u1({url:l,key:u,source:a})}static resolveUrl(p,d){if(p.startsWith("http://")||p.startsWith("https://"))return p;let a=new URL(d);if(p.startsWith("//"))return`${a.protocol}${p}`;if(p.startsWith("/"))return`${a.origin}${p}`;let m=a.pathname.substring(0,a.pathname.lastIndexOf("/")+1);return`${a.origin}${m}${p}`}static findClosestPairs(p){let d=this.findAllMatches(p,this.URL_PATTERNS),a=this.findAllMatches(p,this.KEY_PATTERNS),m=[];for(let D of d)for(let v of a){let l=Math.abs(D.index-v.index);m.push({url:D.text.replace(/['"`;]/g,""),key:v.text.replace(/['"`;]/g,""),distance:l})}return m.sort((D,v)=>D.distance-v.distance)}static findAllMatches(p,d){let a=[];return d.forEach((m)=>{let D;while((D=m.exec(p))!==null)a.push({text:D[0],index:D.index})}),a}}async function $p(p){let{url:d,key:a}=p;if(p.extract){let D={debug:p.debug||!1,json:!1,html:!1,suppressExperimentalWarnings:p.suppressExperimentalWarnings||!1,url:"",key:"",client:P0.createClient("https://temp.supabase.co","temp_key")},v=await b0.extractFromUrl(p.extract,D);if(!v.success)throw Error(`Failed to extract credentials: ${v.error.message}`);if(d=v.value.url,a=v.value.key,v.value.source)G.success(`Extracted credentials from: ${v.value.source}`);else G.success("Extracted credentials from target");if(p.debug)G.debug(D,`URL: ${d}`),G.debug(D,`Key: ${a?.substring(0,20)}...`)}if(!d||!a)throw Error("Either provide --url and --key, or use --extract <url>");let m=P0.createClient(d,a);return{debug:p.debug||!1,json:p.json||!1,html:p.html||!1,suppressExperimentalWarnings:p.suppressExperimentalWarnings||!1,url:d,key:a,client:m}}var q6=h2({level:4,formatOptions:{compact:!0},stdout:process.stderr,stderr:process.stderr}),M6={debug:(p,d,...a)=>{if(p.debug)q6.debug(d,...a)},info:(p,...d)=>q6.info(p,...d),success:(p,...d)=>q6.success(p,...d),warn:(p,...d)=>q6.warn(p,...d),error:(p,...d)=>q6.error(p,...d)},UF=(p)=>{let d=!1;return()=>{if(!d)try{return p()}catch(a){throw d=!0,a}finally{d=!0}}};var Np=!1,Vp=(p)=>{Np=p},Bp=UF(()=>{if(!Np)M6.warn("This feature is experimental and may have bugs. You can suppress this with --suppress-experimental-warnings.")});var Jp="0.0.5";var Yp=Jp;var Zp=new u3;Zp.name("supascan").description("Security analysis tool for Supabase").version(Yp).option("-u, --url <url>","Supabase URL").option("-k, --key <key>","Supabase anon key").option("-s, --schema <schema>","Schema to analyze (default: all schemas)").option("-x, --extract <url>","Extract credentials from JS file URL (experimental)").option("--dump <schema.table|schema>","Dump data from specific table or swagger JSON from schema").option("--limit <number>","Limit rows for dump or RPC results","10").option("--rpc <schema.rpc_name>","Call an RPC function (read-only operations only)").option("--args <json>","JSON arguments for RPC call (use $VAR for environment variables)").option("--json","Output as JSON").option("--html","Generate HTML report").option("-d, --debug","Enable debug mode").option("--explain","Show query execution plan").option("--suppress-experimental-warnings","Suppress experimental warnings").action(async(p)=>{try{if(p.json&&p.html)M6.error("Cannot use --json and --html together. Please choose one."),process.exit(1);if(p.suppressExperimentalWarnings)Vp(!0);if(p.extract)Bp();let d=await $p(p);if(d.debug)M6.debug(d,"CLI Options",p);if(p.rpc){await B9(d,{rpc:p.rpc,args:p.args,limit:p.limit,explain:p.explain});return}if(p.dump){await V9(d,{dump:p.dump,limit:p.limit});return}await N9(d,{schema:p.schema})}catch(d){M6.error("Command failed",d instanceof Error?d.message:String(d)),process.exit(1)}});if(t.main==t.module)await Zp.parseAsync(process.argv);
606
+ `},void 0,!1,void 0,this)]},void 0,!0,void 0,this),$("body",{class:"bg-slate-50 min-h-screen scrollbar-thin m-0 p-0",children:$("div",{class:"w-full min-h-screen px-6 py-6",children:[$("header",{class:"bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6 fade-in",children:$("div",{class:"flex items-center justify-between",children:[$("div",{class:"flex-1",children:[$("h1",{class:"text-2xl font-bold text-slate-900 mb-1 font-mono",children:"Supabase Database Analysis"},void 0,!1,void 0,this),$("p",{class:"text-slate-600 font-mono text-sm",children:["Generated on ",new Date().toLocaleString()]},void 0,!0,void 0,this)]},void 0,!0,void 0,this),$("div",{class:"flex items-center gap-8",children:[$("div",{class:"text-right",children:[$("div",{class:"text-xs text-slate-500 font-mono",children:"Schemas Analyzed"},void 0,!1,void 0,this),$("div",{class:"text-xl font-bold text-emerald-600 font-mono",children:p.schemas.length},void 0,!1,void 0,this)]},void 0,!0,void 0,this),$("button",{onclick:"saveReport()",class:"px-3 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors font-mono text-sm flex items-center gap-2",children:[$("svg",{class:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:$("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3-3m0 0l-3 3m3-3v12"},void 0,!1,void 0,this)},void 0,!1,void 0,this),"Save Report"]},void 0,!0,void 0,this)]},void 0,!0,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this),gm({domain:p.summary.domain,url:d,key:a,metadata:p.summary.metadata,jwtInfo:p.summary.jwtInfo}),$("section",{class:"space-y-8",children:[$("h2",{class:"text-2xl font-bold text-gray-900 mb-6 flex items-center",children:[$("svg",{class:"w-6 h-6 mr-3 text-supabase-green",fill:"currentColor",viewBox:"0 0 20 20",children:$("path",{fillRule:"evenodd",d:"M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z",clipRule:"evenodd"},void 0,!1,void 0,this)},void 0,!1,void 0,this),"Database Analysis"]},void 0,!0,void 0,this),Object.entries(p.schemaDetails).map(([m,D])=>nm({schema:m,analysis:D}))]},void 0,!0,void 0,this),$("footer",{class:"mt-12 text-center text-slate-500 text-sm font-mono",children:$("p",{children:"Generated by supascan - Security analysis tool for Supabase"},void 0,!1,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)},void 0,!1,void 0,this)]},void 0,!0,void 0,this)}}async function N9(p,d){let a=await e4.analyze(p,d.schema);if(!a.success)G.error("Analysis failed",a.error.message),process.exit(1);if(p.json)console.log(JSON.stringify(a.value,null,2));else if(p.html){let m=b4.generateHtmlReport(a.value,p.url,p.key),D=r3();p9(D,m),d9(D),G.success(`HTML report generated: ${D}`)}else vD(a.value)}function vD(p){if(console.log(),console.log(z.default.bold(z.default.cyan("━".repeat(60)))),console.log(z.default.bold(z.default.cyan(" SUPABASE DATABASE ANALYSIS"))),console.log(z.default.bold(z.default.cyan("━".repeat(60)))),console.log(),console.log(z.default.bold(z.default.yellow("TARGET SUMMARY"))),console.log(z.default.dim("─".repeat(20))),console.log(z.default.bold("Domain:"),z.default.white(p.summary.domain)),p.summary.metadata?.service)console.log(z.default.bold("Service:"),z.default.white(p.summary.metadata.service));if(p.summary.metadata?.region)console.log(z.default.bold("Project ID:"),z.default.white(p.summary.metadata.region));if(p.summary.metadata?.title)console.log(z.default.bold("Title:"),z.default.white(p.summary.metadata.title));if(p.summary.metadata?.version)console.log(z.default.bold("Version:"),z.default.white(p.summary.metadata.version));if(p.summary.jwtInfo){if(console.log(),console.log(z.default.bold(z.default.yellow("JWT TOKEN INFO"))),console.log(z.default.dim("─".repeat(20))),p.summary.jwtInfo.iss)console.log(z.default.bold("Issuer:"),z.default.white(p.summary.jwtInfo.iss));if(p.summary.jwtInfo.aud)console.log(z.default.bold("Audience:"),z.default.white(p.summary.jwtInfo.aud));if(p.summary.jwtInfo.role)console.log(z.default.bold("Role:"),z.default.white(p.summary.jwtInfo.role));if(p.summary.jwtInfo.exp){let d=new Date(p.summary.jwtInfo.exp*1000);console.log(z.default.bold("Expires:"),z.default.white(d.toISOString()))}if(p.summary.jwtInfo.iat){let d=new Date(p.summary.jwtInfo.iat*1000);console.log(z.default.bold("Issued:"),z.default.white(d.toISOString()))}}console.log(),console.log(z.default.bold(z.default.cyan("DATABASE ANALYSIS"))),console.log(z.default.dim("─".repeat(20))),console.log(z.default.bold("Schemas discovered:"),z.default.green(p.schemas.length.toString())),console.log(),Object.entries(p.schemaDetails).forEach(([d,a])=>{console.log(z.default.bold(z.default.cyan(`Schema: ${d}`))),console.log();let m=Object.values(a.tableAccess).filter((l)=>l.status==="readable").length,D=Object.values(a.tableAccess).filter((l)=>l.status==="denied").length,v=Object.values(a.tableAccess).filter((l)=>l.status==="empty").length;if(console.log(z.default.bold("Tables:"),z.default.green(a.tables.length.toString())),console.log(z.default.dim(` ${m} exposed • ${v} empty/protected • ${D} denied`)),console.log(),a.tables.length>0)a.tables.forEach((l)=>{let u=a.tableAccess[l],w="",i="";switch(u?.status){case"readable":w=z.default.green("✓"),i=z.default.dim("(data exposed)");break;case"empty":w=z.default.yellow("○"),i=z.default.dim("(0 rows - empty or RLS)");break;case"denied":w=z.default.red("✗"),i=z.default.dim("(access denied)");break}console.log(` ${w} ${z.default.white(l)} ${i}`)});else console.log(z.default.dim(" No tables found"));if(console.log(),console.log(z.default.bold("RPCs:"),z.default.green(a.rpcs.length.toString())),a.rpcFunctions.length>0)a.rpcFunctions.forEach((l)=>{if(console.log(` • ${z.default.white(l.name)}`),l.parameters.length>0)l.parameters.forEach((u)=>{let w=u.required?z.default.red("(required)"):z.default.dim("(optional)"),i=u.format?`${u.type} (${u.format})`:u.type;console.log(` - ${z.default.cyan(u.name)}: ${z.default.yellow(i)} ${w}`)});else console.log(z.default.dim(" No parameters"))});else console.log(z.default.dim(" No RPCs found"));console.log()}),console.log(z.default.bold(z.default.cyan("━".repeat(60)))),console.log()}var j=e2(I6(),1);async function V9(p,d){let a=d.dump.split(".");if(a.length===1&&a[0]){let u=a[0],w=await V1.getSwagger(p,u);if(!w.success)G.error("Failed to get swagger",w.error.message),process.exit(1);if(p.json)console.log(JSON.stringify(w.value,null,2));else uD(u,w.value);return}if(a.length!==2||!a[0]||!a[1])G.error("Invalid format. Use: schema.table or schema"),process.exit(1);let m=a[0],D=a[1],v=parseInt(d.limit),l=await V1.dumpTable(p,m,D,v);if(!l.success)G.error("Failed to dump table",l.error.message),process.exit(1);if(p.json)console.log(JSON.stringify(l.value,null,2));else wD(m,D,l.value)}function uD(p,d){console.log(),console.log(j.default.bold(j.default.cyan("━".repeat(60)))),console.log(j.default.bold(j.default.cyan(` SWAGGER DUMP: ${p}`))),console.log(j.default.bold(j.default.cyan("━".repeat(60)))),console.log(),console.log(JSON.stringify(d,null,2)),console.log(),console.log(j.default.bold(j.default.cyan("━".repeat(60)))),console.log()}function wD(p,d,a){if(console.log(),console.log(j.default.bold(j.default.cyan("━".repeat(60)))),console.log(j.default.bold(j.default.cyan(` TABLE DUMP: ${p}.${d}`))),console.log(j.default.bold(j.default.cyan("━".repeat(60)))),console.log(),console.log(j.default.bold("Total rows:"),j.default.green(a.count.toString())),console.log(j.default.bold("Showing:"),j.default.green(a.rows.length.toString())),console.log(j.default.bold("Columns:"),j.default.green(a.columns.length.toString())),console.log(),console.log(j.default.dim(a.columns.join(", "))),console.log(),a.rows.length>0)console.table(a.rows);else console.log(j.default.dim("No rows found"));console.log(),console.log(j.default.bold(j.default.cyan("━".repeat(60)))),console.log()}var W=e2(I6(),1);async function B9(p,d){let a=d.rpc.split(".");if(a.length!==2||!a[0]||!a[1])G.error("Invalid RPC format. Use: schema.rpc_name"),process.exit(1);let m=a[0],D=a[1],v=await V1.getRPCsWithParameters(p,m),l=null;if(v.success)l=v.value.find((i)=>i.name===`rpc/${D}`)||null;else G.warn("Failed to get RPC functions from schema, proceeding without validation",v.error.message);if(!d.args){iD(m,D,l);return}let u={};try{u=o3(d.args)}catch(i){G.error("Failed to parse RPC arguments",i instanceof Error?i.message:String(i)),process.exit(1)}if(l){let F=l.parameters.filter((C)=>C.required).filter((C)=>!(C.name in u));if(F.length>0)G.error(`Missing required parameters: ${F.map((C)=>C.name).join(", ")}`),process.exit(1)}else G.warn("Skipping parameter validation due to schema introspection failure");let w=await V1.callRPC(p,m,D,u,{get:!0,explain:d.explain,limit:parseInt(d.limit)});if(!w.success)G.error("RPC call failed",w.error.message),process.exit(1);if(p.json)console.log(JSON.stringify(w.value,null,2));else FD(m,D,w.value,d.explain)}function iD(p,d,a){if(console.log(),console.log(W.default.bold(W.default.cyan("━".repeat(60)))),console.log(W.default.bold(W.default.cyan(` RPC HELP: ${p}.${d}`))),console.log(W.default.bold(W.default.cyan("━".repeat(60)))),console.log(),a&&a.parameters.length>0)console.log(W.default.bold("Parameters:")),a.parameters.forEach((m)=>{let D=m.required?W.default.red("(required)"):W.default.dim("(optional)"),v=m.format?`${m.type} (${m.format})`:m.type;if(console.log(` • ${W.default.cyan(m.name)}: ${W.default.yellow(v)} ${D}`),m.description)console.log(W.default.dim(` ${m.description}`))}),console.log(),console.log(W.default.bold("Usage:")),console.log(W.default.dim(`supascan --rpc "${p}.${d}" --args '{"param1": "value1", "param2": "value2"}'`));else if(a)console.log(W.default.dim("No parameters required")),console.log(),console.log(W.default.bold("Usage:")),console.log(W.default.dim(`supascan --rpc "${p}.${d}"`));else console.log(W.default.yellow("⚠️ Schema introspection failed - parameter information unavailable")),console.log(),console.log(W.default.bold("Usage:")),console.log(W.default.dim(`supascan --rpc "${p}.${d}" --args '{"param1": "value1"}'`)),console.log(),console.log(W.default.dim("Note: You can still call the RPC, but parameter validation is disabled"));console.log(),console.log(W.default.bold(W.default.cyan("━".repeat(60)))),console.log()}function FD(p,d,a,m){if(console.log(),console.log(W.default.bold(W.default.cyan("━".repeat(60)))),m)console.log(W.default.bold(W.default.cyan(` QUERY PLAN: ${p}.${d}`)));else console.log(W.default.bold(W.default.cyan(` RPC RESULT: ${p}.${d}`)));if(console.log(W.default.bold(W.default.cyan("━".repeat(60)))),console.log(),m)if(console.log(W.default.bold("Execution Plan:")),console.log(),typeof a==="string")console.log(W.default.yellow(a));else console.log(JSON.stringify(a,null,2));else if(Array.isArray(a))if(console.log(W.default.bold("Results:"),W.default.green(a.length.toString())),console.log(),a.length>0)console.table(a);else console.log(W.default.dim("No results returned"));else if(typeof a==="object"&&a!==null)console.log(W.default.bold("Result:")),console.table([a]);else console.log(W.default.bold("Result:"),W.default.green(String(a)));console.log(),console.log(W.default.bold(W.default.cyan("━".repeat(60)))),console.log()}var P0=e2(Cp(),1);class b0{static URL_PATTERNS=[/https:\/\/[a-z0-9-]+\.supabase\.co\/?/g,/['"`]https:\/\/[a-z0-9-]+\.supabase\.co\/?['"`]/g];static KEY_PATTERNS=[/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,/['"`]eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+['"`]/g];static CREATE_BROWSER_CLIENT_PATTERN=/createBrowserClient\)\s*\(\s*["']([^"']+)["']\s*,\s*["']([^"']+)["']/g;static SCRIPT_SRC_PATTERN=/<script[^>]+src=["']([^"']+)["']/gi;static INLINE_SCRIPT_PATTERN=/<script[^>]*>([\s\S]*?)<\/script>/gi;static async extractFromUrl(p,d){G.debug(d,`Fetching content from: ${p}`);let a=await fetch(p);if(!a.ok)return F1(Error(`Failed to fetch URL: ${a.status} ${a.statusText}`));let m=await a.text(),D=a.headers.get("content-type")??"";G.debug(d,`Fetched ${m.length} bytes (${D})`);let v=D.includes("text/html")||m.trim().startsWith("<!DOCTYPE")||m.trim().startsWith("<html");if(p.endsWith(".js")||D.includes("javascript")||D.includes("ecmascript"))return this.extractFromContent(m,d,p);if(v)return await this.extractFromHtml(m,p,d);return this.extractFromContent(m,d,p)}static async extractFromHtml(p,d,a){G.debug(a,"Detected HTML content, searching for JS files...");let m=Array.from(p.matchAll(this.INLINE_SCRIPT_PATTERN));G.debug(a,`Found ${m.length} inline scripts`);for(let v of m){let l=v[1];if(!l)continue;let u=this.extractFromContent(l,a,"inline script");if(u.success)return G.debug(a,"Found credentials in inline script"),u}let D=Array.from(p.matchAll(this.SCRIPT_SRC_PATTERN));G.debug(a,`Found ${D.length} external scripts`);for(let v of D){let l=v[1];if(!l)continue;let u=this.resolveUrl(l,d);G.debug(a,`Checking script: ${u}`);let w=await fetch(u);if(!w.ok){G.debug(a,`Failed to fetch ${u}`);continue}let i=await w.text(),F=this.extractFromContent(i,a,u);if(F.success)return G.debug(a,`Found credentials in ${u}`),F}return F1(Error("No Supabase credentials found in any scripts"))}static extractFromContent(p,d,a){G.debug(d,"Extracting Supabase credentials...");let m=this.CREATE_BROWSER_CLIENT_PATTERN.exec(p);if(m){let w=m[1],i=m[2];if(w&&i)return G.debug(d,"Found createBrowserClient pattern"),G.debug(d,`Extracted URL: ${w}`),G.debug(d,`Extracted key: ${i.substring(0,20)}...`),u1({url:w,key:i,source:a})}let D=this.findClosestPairs(p);if(G.debug(d,`Found ${D.length} potential URL-key pairs`),D.length===0)return F1(Error("No Supabase URL-key pairs found in content"));let v=D[0];if(!v)return F1(Error("No valid URL-key pairs found"));let{url:l,key:u}=v;return G.debug(d,`Extracted URL: ${l}`),G.debug(d,`Extracted key: ${u.substring(0,20)}...`),u1({url:l,key:u,source:a})}static resolveUrl(p,d){if(p.startsWith("http://")||p.startsWith("https://"))return p;let a=new URL(d);if(p.startsWith("//"))return`${a.protocol}${p}`;if(p.startsWith("/"))return`${a.origin}${p}`;let m=a.pathname.substring(0,a.pathname.lastIndexOf("/")+1);return`${a.origin}${m}${p}`}static findClosestPairs(p){let d=this.findAllMatches(p,this.URL_PATTERNS),a=this.findAllMatches(p,this.KEY_PATTERNS),m=[];for(let D of d)for(let v of a){let l=Math.abs(D.index-v.index);m.push({url:D.text.replace(/['"`;]/g,""),key:v.text.replace(/['"`;]/g,""),distance:l})}return m.sort((D,v)=>D.distance-v.distance)}static findAllMatches(p,d){let a=[];return d.forEach((m)=>{let D;while((D=m.exec(p))!==null)a.push({text:D[0],index:D.index})}),a}}async function $p(p){let{url:d,key:a}=p;if(p.extract){let D={debug:p.debug||!1,json:!1,html:!1,suppressExperimentalWarnings:p.suppressExperimentalWarnings||!1,url:"",key:"",client:P0.createClient("https://temp.supabase.co","temp_key")},v=await b0.extractFromUrl(p.extract,D);if(!v.success)throw Error(`Failed to extract credentials: ${v.error.message}`);if(d=v.value.url,a=v.value.key,v.value.source)G.success(`Extracted credentials from: ${v.value.source}`);else G.success("Extracted credentials from target");if(p.debug)G.debug(D,`URL: ${d}`),G.debug(D,`Key: ${a?.substring(0,20)}...`)}if(!d||!a)throw Error("Either provide --url and --key, or use --extract <url>");let m=P0.createClient(d,a);return{debug:p.debug||!1,json:p.json||!1,html:p.html||!1,suppressExperimentalWarnings:p.suppressExperimentalWarnings||!1,url:d,key:a,client:m}}var q6=h2({level:4,formatOptions:{compact:!0},stdout:process.stderr,stderr:process.stderr}),M6={debug:(p,d,...a)=>{if(p.debug)q6.debug(d,...a)},info:(p,...d)=>q6.info(p,...d),success:(p,...d)=>q6.success(p,...d),warn:(p,...d)=>q6.warn(p,...d),error:(p,...d)=>q6.error(p,...d)},UF=(p)=>{let d=!1;return()=>{if(!d)try{return p()}catch(a){throw d=!0,a}finally{d=!0}}};var Np=!1,Vp=(p)=>{Np=p},Bp=UF(()=>{if(!Np)M6.warn("This feature is experimental and may have bugs. You can suppress this with --suppress-experimental-warnings.")});var Jp="0.0.6";var Yp=Jp;var Zp=new u3;Zp.name("supascan").description("Security analysis tool for Supabase").version(Yp).option("-u, --url <url>","Supabase URL").option("-k, --key <key>","Supabase anon key").option("-s, --schema <schema>","Schema to analyze (default: all schemas)").option("-x, --extract <url>","Extract credentials from JS file URL (experimental)").option("--dump <schema.table|schema>","Dump data from specific table or swagger JSON from schema").option("--limit <number>","Limit rows for dump or RPC results","10").option("--rpc <schema.rpc_name>","Call an RPC function (read-only operations only)").option("--args <json>","JSON arguments for RPC call (use $VAR for environment variables)").option("--json","Output as JSON").option("--html","Generate HTML report").option("-d, --debug","Enable debug mode").option("--explain","Show query execution plan").option("--suppress-experimental-warnings","Suppress experimental warnings").action(async(p)=>{try{if(p.json&&p.html)M6.error("Cannot use --json and --html together. Please choose one."),process.exit(1);if(p.suppressExperimentalWarnings)Vp(!0);if(p.extract)Bp();let d=await $p(p);if(d.debug)M6.debug(d,"CLI Options",p);if(p.rpc){await B9(d,{rpc:p.rpc,args:p.args,limit:p.limit,explain:p.explain});return}if(p.dump){await V9(d,{dump:p.dump,limit:p.limit});return}await N9(d,{schema:p.schema})}catch(d){M6.error("Command failed",d instanceof Error?d.message:String(d)),process.exit(1)}});if(t.main==t.module)await Zp.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supascan",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Security analysis tool for Supabase databases",
5
5
  "main": "dist/supascan.js",
6
6
  "type": "module",