vigile-scan 0.2.5 → 2.1.0
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/LICENSE +190 -0
- package/README.md +5 -5
- package/dist/index.js +2423 -7
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "vigile-scan",
|
|
34
|
-
version: "
|
|
34
|
+
version: "2.1.0",
|
|
35
35
|
description: "Security scanner for AI agent tools \u2014 detect tool poisoning, permission abuse, and supply chain attacks in MCP servers and agent skills",
|
|
36
36
|
main: "dist/index.js",
|
|
37
37
|
bin: {
|
|
@@ -71,15 +71,15 @@ var require_package = __commonJS({
|
|
|
71
71
|
"cursor",
|
|
72
72
|
"copilot"
|
|
73
73
|
],
|
|
74
|
-
author: "Vigile AI
|
|
74
|
+
author: "Vigile AI",
|
|
75
75
|
license: "Apache-2.0",
|
|
76
76
|
homepage: "https://vigile.dev",
|
|
77
77
|
repository: {
|
|
78
78
|
type: "git",
|
|
79
|
-
url: "git+https://github.com/Vigile-ai/vigile-
|
|
79
|
+
url: "git+https://github.com/Vigile-ai/vigile-scan.git"
|
|
80
80
|
},
|
|
81
81
|
bugs: {
|
|
82
|
-
url: "https://github.com/Vigile-ai/vigile-
|
|
82
|
+
url: "https://github.com/Vigile-ai/vigile-scan/issues"
|
|
83
83
|
},
|
|
84
84
|
publishConfig: {
|
|
85
85
|
access: "public"
|
|
@@ -2435,8 +2435,8 @@ function printSentinelUpgrade() {
|
|
|
2435
2435
|
console.log(import_chalk.default.yellow(` \u26A1 Sentinel is a Pro feature. Upgrade to unlock runtime monitoring:`));
|
|
2436
2436
|
console.log(import_chalk.default.cyan(` https://vigile.dev/pricing`));
|
|
2437
2437
|
console.log("");
|
|
2438
|
-
console.log(import_chalk.default.gray(` Pro ($
|
|
2439
|
-
console.log(import_chalk.default.gray(` Pro+ ($
|
|
2438
|
+
console.log(import_chalk.default.gray(` Pro ($29/mo) \u2014 5-min sessions, 3 servers, behavioral detection`));
|
|
2439
|
+
console.log(import_chalk.default.gray(` Pro+ ($99/mo) \u2014 30-min sessions, 10 servers, DNS tunneling & C2 detection`));
|
|
2440
2440
|
console.log("");
|
|
2441
2441
|
}
|
|
2442
2442
|
function printAuthStatus(info) {
|
|
@@ -2685,6 +2685,2335 @@ async function getAuthenticatedClient() {
|
|
|
2685
2685
|
return new VigileApiClient(apiUrl, token);
|
|
2686
2686
|
}
|
|
2687
2687
|
|
|
2688
|
+
// src/scanner/baas/secret-patterns.ts
|
|
2689
|
+
function mask(secret) {
|
|
2690
|
+
if (secret.length <= 8) return "***";
|
|
2691
|
+
return `${secret.slice(0, 4)}***${secret.slice(-4)}`;
|
|
2692
|
+
}
|
|
2693
|
+
var SECRET_PATTERNS = [
|
|
2694
|
+
// ── AI / LLM Providers ──
|
|
2695
|
+
{
|
|
2696
|
+
id: "SP-001",
|
|
2697
|
+
name: "OpenAI API Key",
|
|
2698
|
+
provider: "openai",
|
|
2699
|
+
severity: "critical",
|
|
2700
|
+
pattern: /sk-[A-Za-z0-9]{48}/,
|
|
2701
|
+
description: "OpenAI API key grants full access to GPT-4, Whisper, DALL-E, and all models. Exposed keys result in immediate billing charges.",
|
|
2702
|
+
recommendation: "Rotate at platform.openai.com/api-keys immediately. Move all AI calls to a server-side proxy."
|
|
2703
|
+
},
|
|
2704
|
+
{
|
|
2705
|
+
id: "SP-002",
|
|
2706
|
+
name: "OpenAI Project API Key",
|
|
2707
|
+
provider: "openai",
|
|
2708
|
+
severity: "critical",
|
|
2709
|
+
pattern: /sk-proj-[A-Za-z0-9_-]{80,120}/,
|
|
2710
|
+
description: "OpenAI project-scoped API key. Exposed keys grant API access within the project scope.",
|
|
2711
|
+
recommendation: "Rotate at platform.openai.com/api-keys. Never include in client-side JavaScript."
|
|
2712
|
+
},
|
|
2713
|
+
{
|
|
2714
|
+
id: "SP-003",
|
|
2715
|
+
name: "OpenAI Organization ID",
|
|
2716
|
+
provider: "openai",
|
|
2717
|
+
severity: "medium",
|
|
2718
|
+
pattern: /org-[A-Za-z0-9]{24}/,
|
|
2719
|
+
description: "OpenAI organization identifier. Exposure can leak organizational structure.",
|
|
2720
|
+
recommendation: "Treat as sensitive. Pass server-side only."
|
|
2721
|
+
},
|
|
2722
|
+
{
|
|
2723
|
+
id: "SP-004",
|
|
2724
|
+
name: "Anthropic API Key",
|
|
2725
|
+
provider: "anthropic",
|
|
2726
|
+
severity: "critical",
|
|
2727
|
+
pattern: /sk-ant-api\d{2}-[A-Za-z0-9_-]{93,100}/,
|
|
2728
|
+
description: "Anthropic Claude API key. Grants access to Claude Sonnet, Opus, and Haiku. Full billing exposure.",
|
|
2729
|
+
recommendation: "Rotate at console.anthropic.com. All Claude API calls must be server-side."
|
|
2730
|
+
},
|
|
2731
|
+
{
|
|
2732
|
+
id: "SP-005",
|
|
2733
|
+
name: "Anthropic Session Token",
|
|
2734
|
+
provider: "anthropic",
|
|
2735
|
+
severity: "critical",
|
|
2736
|
+
pattern: /sk-ant-oau\d{2}-[A-Za-z0-9_-]{80,}/,
|
|
2737
|
+
description: "Anthropic OAuth session token. Short-lived but grants full account access if intercepted.",
|
|
2738
|
+
recommendation: "These should never appear in bundles. Remove immediately and revoke session."
|
|
2739
|
+
},
|
|
2740
|
+
{
|
|
2741
|
+
id: "SP-006",
|
|
2742
|
+
name: "HuggingFace User Token",
|
|
2743
|
+
provider: "huggingface",
|
|
2744
|
+
severity: "high",
|
|
2745
|
+
pattern: /hf_[A-Za-z0-9]{34,40}/,
|
|
2746
|
+
description: "HuggingFace API token. Grants access to models, datasets, and inference API.",
|
|
2747
|
+
recommendation: "Rotate at huggingface.co/settings/tokens. Use read-only tokens for public model access."
|
|
2748
|
+
},
|
|
2749
|
+
{
|
|
2750
|
+
id: "SP-007",
|
|
2751
|
+
name: "HuggingFace Org Token",
|
|
2752
|
+
provider: "huggingface",
|
|
2753
|
+
severity: "high",
|
|
2754
|
+
pattern: /api_org_[A-Za-z0-9]{34,40}/,
|
|
2755
|
+
description: "HuggingFace organization API token with org-level privileges.",
|
|
2756
|
+
recommendation: "Rotate at huggingface.co/settings/tokens. Grant minimum required permissions."
|
|
2757
|
+
},
|
|
2758
|
+
{
|
|
2759
|
+
id: "SP-008",
|
|
2760
|
+
name: "Replicate API Token",
|
|
2761
|
+
provider: "replicate",
|
|
2762
|
+
severity: "high",
|
|
2763
|
+
pattern: /r8_[A-Za-z0-9]{35,40}/,
|
|
2764
|
+
description: "Replicate API token. Grants access to run ML models with per-second billing.",
|
|
2765
|
+
recommendation: "Rotate at replicate.com/account/api-tokens. All inference must be server-side."
|
|
2766
|
+
},
|
|
2767
|
+
{
|
|
2768
|
+
id: "SP-009",
|
|
2769
|
+
name: "Cohere API Key",
|
|
2770
|
+
provider: "cohere",
|
|
2771
|
+
severity: "high",
|
|
2772
|
+
pattern: /(?:co-|COHERE_API_KEY['":\s=]+)([A-Za-z0-9]{40,50})/,
|
|
2773
|
+
description: "Cohere API key for language model inference and embeddings.",
|
|
2774
|
+
recommendation: "Rotate at dashboard.cohere.com/api-keys."
|
|
2775
|
+
},
|
|
2776
|
+
{
|
|
2777
|
+
id: "SP-010",
|
|
2778
|
+
name: "Groq API Key",
|
|
2779
|
+
provider: "groq",
|
|
2780
|
+
severity: "high",
|
|
2781
|
+
pattern: /gsk_[A-Za-z0-9]{48,60}/,
|
|
2782
|
+
description: "Groq API key. Grants ultra-fast LLM inference on Llama, Mixtral, Gemma models.",
|
|
2783
|
+
recommendation: "Rotate at console.groq.com/keys. Never ship in client bundles."
|
|
2784
|
+
},
|
|
2785
|
+
{
|
|
2786
|
+
id: "SP-011",
|
|
2787
|
+
name: "Together AI API Key",
|
|
2788
|
+
provider: "together-ai",
|
|
2789
|
+
severity: "high",
|
|
2790
|
+
pattern: /(?:TOGETHER_API_KEY|together_api_key)['":\s=]+([A-Za-z0-9]{64})/i,
|
|
2791
|
+
description: "Together AI API key for open-source LLM inference.",
|
|
2792
|
+
recommendation: "Rotate at api.together.xyz/settings/api-keys."
|
|
2793
|
+
},
|
|
2794
|
+
{
|
|
2795
|
+
id: "SP-012",
|
|
2796
|
+
name: "Mistral AI API Key",
|
|
2797
|
+
provider: "mistral",
|
|
2798
|
+
severity: "high",
|
|
2799
|
+
pattern: /(?:MISTRAL_API_KEY|mistral_api_key)['":\s=]+([A-Za-z0-9]{32,40})/i,
|
|
2800
|
+
description: "Mistral AI API key for Mistral-7B, Mixtral, and Codestral models.",
|
|
2801
|
+
recommendation: "Rotate at console.mistral.ai/api-keys."
|
|
2802
|
+
},
|
|
2803
|
+
{
|
|
2804
|
+
id: "SP-013",
|
|
2805
|
+
name: "Perplexity API Key",
|
|
2806
|
+
provider: "perplexity",
|
|
2807
|
+
severity: "high",
|
|
2808
|
+
pattern: /pplx-[A-Za-z0-9]{48,60}/,
|
|
2809
|
+
description: "Perplexity AI API key for search-augmented language model access.",
|
|
2810
|
+
recommendation: "Rotate at perplexity.ai/settings/api."
|
|
2811
|
+
},
|
|
2812
|
+
{
|
|
2813
|
+
id: "SP-014",
|
|
2814
|
+
name: "Cerebras API Key",
|
|
2815
|
+
provider: "cerebras",
|
|
2816
|
+
severity: "high",
|
|
2817
|
+
pattern: /csk-[A-Za-z0-9]{40,60}/,
|
|
2818
|
+
description: "Cerebras API key for ultra-fast inference on Llama models.",
|
|
2819
|
+
recommendation: "Rotate at cloud.cerebras.ai. All inference must be server-side."
|
|
2820
|
+
},
|
|
2821
|
+
{
|
|
2822
|
+
id: "SP-015",
|
|
2823
|
+
name: "OpenRouter API Key",
|
|
2824
|
+
provider: "openrouter",
|
|
2825
|
+
severity: "high",
|
|
2826
|
+
pattern: /sk-or-[A-Za-z0-9]{40,60}/,
|
|
2827
|
+
description: "OpenRouter API key providing access to 100+ AI models through a unified API.",
|
|
2828
|
+
recommendation: "Rotate at openrouter.ai/keys."
|
|
2829
|
+
},
|
|
2830
|
+
{
|
|
2831
|
+
id: "SP-016",
|
|
2832
|
+
name: "ElevenLabs API Key",
|
|
2833
|
+
provider: "elevenlabs",
|
|
2834
|
+
severity: "high",
|
|
2835
|
+
pattern: /(?:ELEVENLABS_API_KEY|xi-api-key)['":\s=]+([A-Za-z0-9_-]{32,50})/i,
|
|
2836
|
+
description: "ElevenLabs voice synthesis API key. Grants text-to-speech and voice cloning access.",
|
|
2837
|
+
recommendation: "Rotate at elevenlabs.io/profile/api-key."
|
|
2838
|
+
},
|
|
2839
|
+
{
|
|
2840
|
+
id: "SP-017",
|
|
2841
|
+
name: "AssemblyAI API Key",
|
|
2842
|
+
provider: "assemblyai",
|
|
2843
|
+
severity: "high",
|
|
2844
|
+
pattern: /(?:ASSEMBLYAI_API_KEY)['":\s=]+([a-f0-9]{40,50})/i,
|
|
2845
|
+
description: "AssemblyAI transcription API key. Grants access to speech-to-text and audio intelligence.",
|
|
2846
|
+
recommendation: "Rotate at www.assemblyai.com/app/account."
|
|
2847
|
+
},
|
|
2848
|
+
{
|
|
2849
|
+
id: "SP-018",
|
|
2850
|
+
name: "Deepgram API Key",
|
|
2851
|
+
provider: "deepgram",
|
|
2852
|
+
severity: "high",
|
|
2853
|
+
pattern: /(?:DEEPGRAM_API_KEY)['":\s=]+([a-f0-9]{40,50})/i,
|
|
2854
|
+
description: "Deepgram speech recognition API key.",
|
|
2855
|
+
recommendation: "Rotate at console.deepgram.com/project/api-key."
|
|
2856
|
+
},
|
|
2857
|
+
{
|
|
2858
|
+
id: "SP-019",
|
|
2859
|
+
name: "Stability AI API Key",
|
|
2860
|
+
provider: "stability-ai",
|
|
2861
|
+
severity: "high",
|
|
2862
|
+
pattern: /sk-[A-Za-z0-9]{32,50}(?=['"}\s])/,
|
|
2863
|
+
description: "Stability AI key for image generation (Stable Diffusion). Overlaps with sk- prefix \u2014 validate by context.",
|
|
2864
|
+
recommendation: "Rotate at platform.stability.ai/account/keys."
|
|
2865
|
+
},
|
|
2866
|
+
{
|
|
2867
|
+
id: "SP-020",
|
|
2868
|
+
name: "LangSmith API Key",
|
|
2869
|
+
provider: "langsmith",
|
|
2870
|
+
severity: "medium",
|
|
2871
|
+
pattern: /ls__[A-Za-z0-9_-]{32,50}/,
|
|
2872
|
+
description: "LangSmith API key for LLM observability and tracing.",
|
|
2873
|
+
recommendation: "Rotate at smith.langchain.com/settings."
|
|
2874
|
+
},
|
|
2875
|
+
{
|
|
2876
|
+
id: "SP-021",
|
|
2877
|
+
name: "Weights & Biases API Key",
|
|
2878
|
+
provider: "wandb",
|
|
2879
|
+
severity: "medium",
|
|
2880
|
+
pattern: /(?:WANDB_API_KEY)['":\s=]+([a-f0-9]{40})/i,
|
|
2881
|
+
description: "Weights & Biases ML experiment tracking API key.",
|
|
2882
|
+
recommendation: "Rotate at wandb.ai/authorize."
|
|
2883
|
+
},
|
|
2884
|
+
{
|
|
2885
|
+
id: "SP-022",
|
|
2886
|
+
name: "Helicone API Key",
|
|
2887
|
+
provider: "helicone",
|
|
2888
|
+
severity: "medium",
|
|
2889
|
+
pattern: /sk-helicone-[A-Za-z0-9_-]{30,60}/,
|
|
2890
|
+
description: "Helicone LLM observability API key for caching and monitoring AI calls.",
|
|
2891
|
+
recommendation: "Rotate at helicone.ai/settings."
|
|
2892
|
+
},
|
|
2893
|
+
{
|
|
2894
|
+
id: "SP-023",
|
|
2895
|
+
name: "E2B API Key",
|
|
2896
|
+
provider: "e2b",
|
|
2897
|
+
severity: "high",
|
|
2898
|
+
pattern: /e2b_[A-Za-z0-9_-]{30,50}/,
|
|
2899
|
+
description: "E2B sandbox API key. Grants ability to spin up code execution sandboxes \u2014 billing risk.",
|
|
2900
|
+
recommendation: "Rotate at e2b.dev/dashboard."
|
|
2901
|
+
},
|
|
2902
|
+
{
|
|
2903
|
+
id: "SP-024",
|
|
2904
|
+
name: "Tavily Search API Key",
|
|
2905
|
+
provider: "tavily",
|
|
2906
|
+
severity: "medium",
|
|
2907
|
+
pattern: /tvly-[A-Za-z0-9_-]{30,50}/,
|
|
2908
|
+
description: "Tavily AI search API key.",
|
|
2909
|
+
recommendation: "Rotate at app.tavily.com."
|
|
2910
|
+
},
|
|
2911
|
+
{
|
|
2912
|
+
id: "SP-025",
|
|
2913
|
+
name: "Firecrawl API Key",
|
|
2914
|
+
provider: "firecrawl",
|
|
2915
|
+
severity: "medium",
|
|
2916
|
+
pattern: /fc-[A-Za-z0-9]{32,50}/,
|
|
2917
|
+
description: "Firecrawl web scraping API key.",
|
|
2918
|
+
recommendation: "Rotate at firecrawl.dev/account."
|
|
2919
|
+
},
|
|
2920
|
+
{
|
|
2921
|
+
id: "SP-026",
|
|
2922
|
+
name: "Jina AI API Key",
|
|
2923
|
+
provider: "jina",
|
|
2924
|
+
severity: "medium",
|
|
2925
|
+
pattern: /jina_[A-Za-z0-9_-]{50,80}/,
|
|
2926
|
+
description: "Jina AI API key for embeddings and search.",
|
|
2927
|
+
recommendation: "Rotate at jina.ai/dashboard."
|
|
2928
|
+
},
|
|
2929
|
+
{
|
|
2930
|
+
id: "SP-027",
|
|
2931
|
+
name: "Apify API Token",
|
|
2932
|
+
provider: "apify",
|
|
2933
|
+
severity: "medium",
|
|
2934
|
+
pattern: /apify_api_[A-Za-z0-9]{40,60}/,
|
|
2935
|
+
description: "Apify web scraping platform API token.",
|
|
2936
|
+
recommendation: "Rotate at console.apify.com/account/integrations."
|
|
2937
|
+
},
|
|
2938
|
+
// ── Cloud Providers ──
|
|
2939
|
+
{
|
|
2940
|
+
id: "SP-028",
|
|
2941
|
+
name: "AWS Access Key ID",
|
|
2942
|
+
provider: "aws",
|
|
2943
|
+
severity: "critical",
|
|
2944
|
+
pattern: /AKIA[A-Z0-9]{16}/,
|
|
2945
|
+
description: "AWS IAM Access Key ID. Pair this with a secret key to authenticate against any AWS service.",
|
|
2946
|
+
recommendation: "Rotate immediately in AWS IAM console. Enable MFA. Use instance profiles instead of long-lived keys."
|
|
2947
|
+
},
|
|
2948
|
+
{
|
|
2949
|
+
id: "SP-029",
|
|
2950
|
+
name: "AWS Secret Access Key",
|
|
2951
|
+
provider: "aws",
|
|
2952
|
+
severity: "critical",
|
|
2953
|
+
pattern: /(?:aws[_-]?secret[_-]?(?:access[_-]?)?key|AWS_SECRET(?:_ACCESS_KEY)?)['":\s=]+([A-Za-z0-9/+=]{40})/i,
|
|
2954
|
+
description: "AWS IAM Secret Access Key. Combined with key ID, provides full AWS service access.",
|
|
2955
|
+
recommendation: "Rotate immediately. Audit CloudTrail for unauthorized usage. Use IAM roles instead."
|
|
2956
|
+
},
|
|
2957
|
+
{
|
|
2958
|
+
id: "SP-030",
|
|
2959
|
+
name: "AWS Session Token",
|
|
2960
|
+
provider: "aws",
|
|
2961
|
+
severity: "high",
|
|
2962
|
+
pattern: /(?:AWS_SESSION_TOKEN|aws_session_token)['":\s=]+([A-Za-z0-9/+=]{200,600})/i,
|
|
2963
|
+
description: "Temporary AWS STS session token. Short-lived but grants API access until expiry.",
|
|
2964
|
+
recommendation: "These expire automatically but should not be in client code."
|
|
2965
|
+
},
|
|
2966
|
+
{
|
|
2967
|
+
id: "SP-031",
|
|
2968
|
+
name: "Google Cloud API Key",
|
|
2969
|
+
provider: "google-cloud",
|
|
2970
|
+
severity: "critical",
|
|
2971
|
+
pattern: /AIza[0-9A-Za-z\-_]{35}/,
|
|
2972
|
+
description: "Google Cloud / Firebase API key. Grants access to Google Maps, Firebase, YouTube, and other Google APIs depending on restrictions.",
|
|
2973
|
+
recommendation: "Add API key restrictions in Google Cloud Console. Rotate at console.cloud.google.com/apis/credentials."
|
|
2974
|
+
},
|
|
2975
|
+
{
|
|
2976
|
+
id: "SP-032",
|
|
2977
|
+
name: "Google Service Account Key",
|
|
2978
|
+
provider: "google-cloud",
|
|
2979
|
+
severity: "critical",
|
|
2980
|
+
pattern: /"private_key"\s*:\s*"-----BEGIN RSA PRIVATE KEY/,
|
|
2981
|
+
description: "Google Cloud service account private key embedded in source. Grants IAM service account impersonation.",
|
|
2982
|
+
recommendation: "Revoke the service account key immediately. Use Workload Identity or ADC instead of key files."
|
|
2983
|
+
},
|
|
2984
|
+
{
|
|
2985
|
+
id: "SP-033",
|
|
2986
|
+
name: "Google OAuth Client Secret",
|
|
2987
|
+
provider: "google-cloud",
|
|
2988
|
+
severity: "high",
|
|
2989
|
+
pattern: /GOCSPX-[A-Za-z0-9_-]{28}/,
|
|
2990
|
+
description: "Google OAuth 2.0 client secret. Enables impersonation of your OAuth application.",
|
|
2991
|
+
recommendation: "Rotate at console.cloud.google.com/apis/credentials. Keep client secrets server-side only."
|
|
2992
|
+
},
|
|
2993
|
+
{
|
|
2994
|
+
id: "SP-034",
|
|
2995
|
+
name: "Firebase Web API Key",
|
|
2996
|
+
provider: "firebase",
|
|
2997
|
+
severity: "medium",
|
|
2998
|
+
pattern: /(?:NEXT_PUBLIC_FIREBASE_API_KEY|VITE_FIREBASE_API_KEY|firebase(?:Config)?\.apiKey)['":\s=]+["']?(AIza[0-9A-Za-z\-_]{35})/i,
|
|
2999
|
+
description: "Firebase Web API Key in environment variable. The key itself is semi-public but should be paired with proper security rules.",
|
|
3000
|
+
recommendation: "Restrict key usage in Firebase Console. Ensure Firestore and Storage rules are not open."
|
|
3001
|
+
},
|
|
3002
|
+
{
|
|
3003
|
+
id: "SP-035",
|
|
3004
|
+
name: "Firebase Service Account",
|
|
3005
|
+
provider: "firebase",
|
|
3006
|
+
severity: "critical",
|
|
3007
|
+
pattern: /(?:FIREBASE_SERVICE_ACCOUNT|firebase[_-]admin[_-]sdk)['":\s=]+/i,
|
|
3008
|
+
description: "Firebase Admin SDK service account credentials. Grants admin-level access bypassing all security rules.",
|
|
3009
|
+
recommendation: "Never include Firebase Admin credentials in client bundles. Use client SDK on frontend, Admin SDK on backend only."
|
|
3010
|
+
},
|
|
3011
|
+
{
|
|
3012
|
+
id: "SP-036",
|
|
3013
|
+
name: "Supabase Service Role Key",
|
|
3014
|
+
provider: "supabase",
|
|
3015
|
+
severity: "critical",
|
|
3016
|
+
pattern: /(?:SUPABASE_SERVICE_ROLE_KEY|SUPABASE_SERVICE_KEY|supabase[_-]service[_-]role)['":\s=]+["']?(eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/i,
|
|
3017
|
+
description: "Supabase service role key. Bypasses ALL Row Level Security policies \u2014 full database admin access.",
|
|
3018
|
+
recommendation: "Immediately rotate in Supabase dashboard > Project Settings > API. This key MUST NEVER be in client code."
|
|
3019
|
+
},
|
|
3020
|
+
{
|
|
3021
|
+
id: "SP-037",
|
|
3022
|
+
name: "Supabase Anon Key (Public)",
|
|
3023
|
+
provider: "supabase",
|
|
3024
|
+
severity: "low",
|
|
3025
|
+
pattern: /(?:NEXT_PUBLIC_SUPABASE_ANON_KEY|VITE_SUPABASE_ANON_KEY|supabase[_-]anon[_-]key)['":\s=]+["']?(eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/i,
|
|
3026
|
+
description: "Supabase anon/public key. This is designed to be public, but without proper RLS policies, it can expose your data.",
|
|
3027
|
+
recommendation: "Safe to be public ONLY if RLS policies are correctly configured on all tables."
|
|
3028
|
+
},
|
|
3029
|
+
{
|
|
3030
|
+
id: "SP-038",
|
|
3031
|
+
name: "Supabase URL",
|
|
3032
|
+
provider: "supabase",
|
|
3033
|
+
severity: "low",
|
|
3034
|
+
pattern: /https:\/\/[a-z0-9]{20}\.supabase\.co/,
|
|
3035
|
+
description: "Supabase project URL. The unique identifier for your Supabase project.",
|
|
3036
|
+
recommendation: "Exposure is low-risk, but combine with anon key check to assess full exposure surface."
|
|
3037
|
+
},
|
|
3038
|
+
{
|
|
3039
|
+
id: "SP-039",
|
|
3040
|
+
name: "Azure Storage Connection String",
|
|
3041
|
+
provider: "azure",
|
|
3042
|
+
severity: "critical",
|
|
3043
|
+
pattern: /DefaultEndpointsProtocol=https;AccountName=[^;]+;AccountKey=[A-Za-z0-9+/=]{88}/,
|
|
3044
|
+
description: "Azure Storage Account connection string. Grants full read/write access to all blobs, queues, and tables.",
|
|
3045
|
+
recommendation: "Rotate in Azure Portal > Storage Account > Access Keys. Use Managed Identity or SAS tokens instead."
|
|
3046
|
+
},
|
|
3047
|
+
{
|
|
3048
|
+
id: "SP-040",
|
|
3049
|
+
name: "Azure Subscription Key",
|
|
3050
|
+
provider: "azure",
|
|
3051
|
+
severity: "high",
|
|
3052
|
+
pattern: /(?:Ocp-Apim-Subscription-Key|azure[_-]?subscription[_-]?key)['":\s=]+([A-Za-z0-9]{32})/i,
|
|
3053
|
+
description: "Azure API Management subscription key.",
|
|
3054
|
+
recommendation: "Rotate in Azure API Management portal."
|
|
3055
|
+
},
|
|
3056
|
+
{
|
|
3057
|
+
id: "SP-041",
|
|
3058
|
+
name: "Azure AD Client Secret",
|
|
3059
|
+
provider: "azure",
|
|
3060
|
+
severity: "critical",
|
|
3061
|
+
pattern: /(?:AZURE_CLIENT_SECRET|azure[_-]?client[_-]?secret)['":\s=]+([A-Za-z0-9~.@-]{34,40})/i,
|
|
3062
|
+
description: "Azure Active Directory application client secret. Grants OAuth access as the application.",
|
|
3063
|
+
recommendation: "Rotate in Azure AD > App Registrations > Certificates & Secrets."
|
|
3064
|
+
},
|
|
3065
|
+
{
|
|
3066
|
+
id: "SP-042",
|
|
3067
|
+
name: "Cloudflare API Token",
|
|
3068
|
+
provider: "cloudflare",
|
|
3069
|
+
severity: "critical",
|
|
3070
|
+
pattern: /(?:CLOUDFLARE_API_TOKEN|CF_API_TOKEN)['":\s=]+([A-Za-z0-9_-]{40})/i,
|
|
3071
|
+
description: "Cloudflare API token. Grants access to DNS, Workers, Pages, and account settings.",
|
|
3072
|
+
recommendation: "Rotate at dash.cloudflare.com/profile/api-tokens. Use scoped tokens with minimum permissions."
|
|
3073
|
+
},
|
|
3074
|
+
{
|
|
3075
|
+
id: "SP-043",
|
|
3076
|
+
name: "Cloudflare API Key (Global)",
|
|
3077
|
+
provider: "cloudflare",
|
|
3078
|
+
severity: "critical",
|
|
3079
|
+
pattern: /(?:CLOUDFLARE_API_KEY|CF_API_KEY)['":\s=]+([a-f0-9]{37})/i,
|
|
3080
|
+
description: "Cloudflare Global API Key. Grants full account access \u2014 more dangerous than API tokens.",
|
|
3081
|
+
recommendation: "Rotate at dash.cloudflare.com/profile/api-tokens. Prefer scoped API tokens over global keys."
|
|
3082
|
+
},
|
|
3083
|
+
{
|
|
3084
|
+
id: "SP-044",
|
|
3085
|
+
name: "Cloudflare Workers KV Namespace",
|
|
3086
|
+
provider: "cloudflare",
|
|
3087
|
+
severity: "low",
|
|
3088
|
+
pattern: /(?:KV_NAMESPACE_ID|CLOUDFLARE_KV_NAMESPACE)['":\s=]+([a-f0-9]{32})/i,
|
|
3089
|
+
description: "Cloudflare Workers KV namespace identifier.",
|
|
3090
|
+
recommendation: "Low risk alone, but combined with API credentials allows data access."
|
|
3091
|
+
},
|
|
3092
|
+
{
|
|
3093
|
+
id: "SP-045",
|
|
3094
|
+
name: "Vercel API Token",
|
|
3095
|
+
provider: "vercel",
|
|
3096
|
+
severity: "critical",
|
|
3097
|
+
pattern: /(?:VERCEL_TOKEN|vercel[_-]token)['":\s=]+([A-Za-z0-9]{24})/i,
|
|
3098
|
+
description: "Vercel deployment API token. Grants ability to deploy, manage domains, and access secrets.",
|
|
3099
|
+
recommendation: "Rotate at vercel.com/account/tokens."
|
|
3100
|
+
},
|
|
3101
|
+
{
|
|
3102
|
+
id: "SP-046",
|
|
3103
|
+
name: "Vercel Access Token",
|
|
3104
|
+
provider: "vercel",
|
|
3105
|
+
severity: "critical",
|
|
3106
|
+
pattern: /vercel_[A-Za-z0-9]{40,60}/i,
|
|
3107
|
+
description: "Vercel access token for CLI or API authentication.",
|
|
3108
|
+
recommendation: "Rotate at vercel.com/account/tokens."
|
|
3109
|
+
},
|
|
3110
|
+
{
|
|
3111
|
+
id: "SP-047",
|
|
3112
|
+
name: "DigitalOcean Personal Access Token",
|
|
3113
|
+
provider: "digitalocean",
|
|
3114
|
+
severity: "critical",
|
|
3115
|
+
pattern: /dop_v1_[a-f0-9]{64}/,
|
|
3116
|
+
description: "DigitalOcean personal access token. Grants full account access to droplets, databases, and storage.",
|
|
3117
|
+
recommendation: "Rotate at cloud.digitalocean.com/account/api/tokens."
|
|
3118
|
+
},
|
|
3119
|
+
{
|
|
3120
|
+
id: "SP-048",
|
|
3121
|
+
name: "Railway Token",
|
|
3122
|
+
provider: "railway",
|
|
3123
|
+
severity: "high",
|
|
3124
|
+
pattern: /(?:RAILWAY_TOKEN|railway[_-]token)['":\s=]+([A-Za-z0-9_-]{30,50})/i,
|
|
3125
|
+
description: "Railway deployment platform API token.",
|
|
3126
|
+
recommendation: "Rotate at railway.app/account/tokens."
|
|
3127
|
+
},
|
|
3128
|
+
{
|
|
3129
|
+
id: "SP-049",
|
|
3130
|
+
name: "Fly.io API Token",
|
|
3131
|
+
provider: "flyio",
|
|
3132
|
+
severity: "high",
|
|
3133
|
+
pattern: /fo1_[A-Za-z0-9_-]{40,80}/,
|
|
3134
|
+
description: "Fly.io API token for machine deployment and management.",
|
|
3135
|
+
recommendation: "Rotate with: flyctl tokens revoke <token>"
|
|
3136
|
+
},
|
|
3137
|
+
// ── Payment Providers ──
|
|
3138
|
+
{
|
|
3139
|
+
id: "SP-050",
|
|
3140
|
+
name: "Stripe Secret Key (Live)",
|
|
3141
|
+
provider: "stripe",
|
|
3142
|
+
severity: "critical",
|
|
3143
|
+
pattern: /sk_live_[A-Za-z0-9]{24,100}/,
|
|
3144
|
+
description: "Stripe live-mode secret key. Grants full access to create charges, issue refunds, and access customer data.",
|
|
3145
|
+
recommendation: "Rotate immediately at dashboard.stripe.com/apikeys. Never expose in client code."
|
|
3146
|
+
},
|
|
3147
|
+
{
|
|
3148
|
+
id: "SP-051",
|
|
3149
|
+
name: "Stripe Publishable Key (Live)",
|
|
3150
|
+
provider: "stripe",
|
|
3151
|
+
severity: "low",
|
|
3152
|
+
pattern: /pk_live_[A-Za-z0-9]{24,100}/,
|
|
3153
|
+
description: "Stripe live publishable key. This is meant to be public for frontend use.",
|
|
3154
|
+
recommendation: "This key is designed to be public. Ensure it is not confused with the secret key."
|
|
3155
|
+
},
|
|
3156
|
+
{
|
|
3157
|
+
id: "SP-052",
|
|
3158
|
+
name: "Stripe Restricted Key (Live)",
|
|
3159
|
+
provider: "stripe",
|
|
3160
|
+
severity: "high",
|
|
3161
|
+
pattern: /rk_live_[A-Za-z0-9]{24,100}/,
|
|
3162
|
+
description: "Stripe live restricted key with limited permissions. Scope depends on configuration.",
|
|
3163
|
+
recommendation: "Rotate at dashboard.stripe.com/apikeys. Audit which permissions are granted."
|
|
3164
|
+
},
|
|
3165
|
+
{
|
|
3166
|
+
id: "SP-053",
|
|
3167
|
+
name: "Stripe Webhook Secret",
|
|
3168
|
+
provider: "stripe",
|
|
3169
|
+
severity: "high",
|
|
3170
|
+
pattern: /whsec_[A-Za-z0-9]{32,60}/,
|
|
3171
|
+
description: "Stripe webhook endpoint signing secret. Allows forging webhook events.",
|
|
3172
|
+
recommendation: "Rotate webhook secret at dashboard.stripe.com/webhooks. Keep server-side only."
|
|
3173
|
+
},
|
|
3174
|
+
{
|
|
3175
|
+
id: "SP-054",
|
|
3176
|
+
name: "PayPal Client Secret",
|
|
3177
|
+
provider: "paypal",
|
|
3178
|
+
severity: "critical",
|
|
3179
|
+
pattern: /(?:PAYPAL_CLIENT_SECRET|paypal[_-]secret)['":\s=]+([A-Za-z0-9_-]{60,80})/i,
|
|
3180
|
+
description: "PayPal REST API client secret. Grants ability to process payments and access merchant data.",
|
|
3181
|
+
recommendation: "Rotate at developer.paypal.com/developer/applications."
|
|
3182
|
+
},
|
|
3183
|
+
{
|
|
3184
|
+
id: "SP-055",
|
|
3185
|
+
name: "PayPal Access Token",
|
|
3186
|
+
provider: "paypal",
|
|
3187
|
+
severity: "high",
|
|
3188
|
+
pattern: /(?:A21AA|Bearer\s+)[A-Za-z0-9_-]{80,120}/,
|
|
3189
|
+
description: "PayPal OAuth access token. Short-lived but grants API access to payment operations.",
|
|
3190
|
+
recommendation: "Access tokens expire but should not be cached in client code."
|
|
3191
|
+
},
|
|
3192
|
+
{
|
|
3193
|
+
id: "SP-056",
|
|
3194
|
+
name: "Plaid Secret Key",
|
|
3195
|
+
provider: "plaid",
|
|
3196
|
+
severity: "critical",
|
|
3197
|
+
pattern: /(?:PLAID_SECRET|plaid[_-]secret)['":\s=]+([a-f0-9]{30,40})/i,
|
|
3198
|
+
description: "Plaid financial data API secret key. Grants access to bank account and transaction data.",
|
|
3199
|
+
recommendation: "Rotate at dashboard.plaid.com/team/keys."
|
|
3200
|
+
},
|
|
3201
|
+
{
|
|
3202
|
+
id: "SP-057",
|
|
3203
|
+
name: "Plaid Client ID",
|
|
3204
|
+
provider: "plaid",
|
|
3205
|
+
severity: "medium",
|
|
3206
|
+
pattern: /(?:PLAID_CLIENT_ID|plaid[_-]client[_-]id)['":\s=]+([a-f0-9]{24})/i,
|
|
3207
|
+
description: "Plaid application client ID.",
|
|
3208
|
+
recommendation: "Low risk alone, but combined with secret key grants bank data access."
|
|
3209
|
+
},
|
|
3210
|
+
{
|
|
3211
|
+
id: "SP-058",
|
|
3212
|
+
name: "Square Access Token",
|
|
3213
|
+
provider: "square",
|
|
3214
|
+
severity: "critical",
|
|
3215
|
+
pattern: /(?:EAA|sq0atp-)[A-Za-z0-9_-]{22,44}/,
|
|
3216
|
+
description: "Square payment processing access token.",
|
|
3217
|
+
recommendation: "Rotate at developer.squareup.com/apps."
|
|
3218
|
+
},
|
|
3219
|
+
{
|
|
3220
|
+
id: "SP-059",
|
|
3221
|
+
name: "Shopify Admin API Key",
|
|
3222
|
+
provider: "shopify",
|
|
3223
|
+
severity: "critical",
|
|
3224
|
+
pattern: /shpat_[a-fA-F0-9]{32}/,
|
|
3225
|
+
description: "Shopify Admin API access token. Grants full store management capabilities.",
|
|
3226
|
+
recommendation: "Revoke at Shopify Admin > Settings > Apps and Sales Channels > API."
|
|
3227
|
+
},
|
|
3228
|
+
{
|
|
3229
|
+
id: "SP-060",
|
|
3230
|
+
name: "Shopify Storefront Token",
|
|
3231
|
+
provider: "shopify",
|
|
3232
|
+
severity: "medium",
|
|
3233
|
+
pattern: /shpss_[a-fA-F0-9]{32}/,
|
|
3234
|
+
description: "Shopify Storefront API access token. More limited than admin token.",
|
|
3235
|
+
recommendation: "Designed for client use but ensure scopes are minimum necessary."
|
|
3236
|
+
},
|
|
3237
|
+
// ── Communication / Messaging ──
|
|
3238
|
+
{
|
|
3239
|
+
id: "SP-061",
|
|
3240
|
+
name: "Slack Bot Token",
|
|
3241
|
+
provider: "slack",
|
|
3242
|
+
severity: "high",
|
|
3243
|
+
pattern: /xoxb-[0-9]+-[0-9]+-[A-Za-z0-9]+/,
|
|
3244
|
+
description: "Slack bot OAuth token. Grants access to post messages, read channels, and perform bot actions.",
|
|
3245
|
+
recommendation: "Rotate at api.slack.com/apps. Scope tokens to minimum required permissions."
|
|
3246
|
+
},
|
|
3247
|
+
{
|
|
3248
|
+
id: "SP-062",
|
|
3249
|
+
name: "Slack User Token",
|
|
3250
|
+
provider: "slack",
|
|
3251
|
+
severity: "critical",
|
|
3252
|
+
pattern: /xoxp-[0-9]+-[0-9]+-[0-9]+-[A-Za-z0-9]+/,
|
|
3253
|
+
description: "Slack user OAuth token. Acts as the user \u2014 can read all accessible messages and DMs.",
|
|
3254
|
+
recommendation: "Rotate at api.slack.com/apps. User tokens should never be in application code."
|
|
3255
|
+
},
|
|
3256
|
+
{
|
|
3257
|
+
id: "SP-063",
|
|
3258
|
+
name: "Slack App-Level Token",
|
|
3259
|
+
provider: "slack",
|
|
3260
|
+
severity: "high",
|
|
3261
|
+
pattern: /xapp-[0-9]+-[A-Za-z0-9]+-[A-Za-z0-9]+/,
|
|
3262
|
+
description: "Slack app-level token for socket mode and org-wide operations.",
|
|
3263
|
+
recommendation: "Rotate at api.slack.com/apps."
|
|
3264
|
+
},
|
|
3265
|
+
{
|
|
3266
|
+
id: "SP-064",
|
|
3267
|
+
name: "Slack Webhook URL",
|
|
3268
|
+
provider: "slack",
|
|
3269
|
+
severity: "medium",
|
|
3270
|
+
pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/,
|
|
3271
|
+
description: "Slack incoming webhook URL. Allows posting messages to a specific channel.",
|
|
3272
|
+
recommendation: "Rotate at api.slack.com/apps > Incoming Webhooks. Treat as sensitive."
|
|
3273
|
+
},
|
|
3274
|
+
{
|
|
3275
|
+
id: "SP-065",
|
|
3276
|
+
name: "Discord Bot Token",
|
|
3277
|
+
provider: "discord",
|
|
3278
|
+
severity: "critical",
|
|
3279
|
+
pattern: /[MN][A-Za-z0-9_-]{23}\.[\w-]{6}\.[\w-]{27,38}/,
|
|
3280
|
+
description: "Discord bot token. Grants full bot account access \u2014 can read all visible messages and take actions.",
|
|
3281
|
+
recommendation: "Regenerate at discord.com/developers/applications. Immediately revoke if exposed."
|
|
3282
|
+
},
|
|
3283
|
+
{
|
|
3284
|
+
id: "SP-066",
|
|
3285
|
+
name: "Discord Webhook URL",
|
|
3286
|
+
provider: "discord",
|
|
3287
|
+
severity: "medium",
|
|
3288
|
+
pattern: /https:\/\/discord(?:app)?\.com\/api\/webhooks\/[0-9]+\/[A-Za-z0-9_-]+/,
|
|
3289
|
+
description: "Discord webhook URL. Allows posting to a channel without a bot account.",
|
|
3290
|
+
recommendation: "Delete and recreate the webhook at the Discord channel settings."
|
|
3291
|
+
},
|
|
3292
|
+
{
|
|
3293
|
+
id: "SP-067",
|
|
3294
|
+
name: "Discord Client Secret",
|
|
3295
|
+
provider: "discord",
|
|
3296
|
+
severity: "high",
|
|
3297
|
+
pattern: /(?:DISCORD_CLIENT_SECRET|discord[_-]secret)['":\s=]+([A-Za-z0-9_-]{32})/i,
|
|
3298
|
+
description: "Discord OAuth application client secret.",
|
|
3299
|
+
recommendation: "Rotate at discord.com/developers/applications."
|
|
3300
|
+
},
|
|
3301
|
+
{
|
|
3302
|
+
id: "SP-068",
|
|
3303
|
+
name: "Twilio Account SID",
|
|
3304
|
+
provider: "twilio",
|
|
3305
|
+
severity: "medium",
|
|
3306
|
+
pattern: /AC[a-fA-F0-9]{32}/,
|
|
3307
|
+
description: "Twilio Account SID. Identifier needed with auth token to access SMS, voice, and messaging APIs.",
|
|
3308
|
+
recommendation: "The SID alone is low risk \u2014 it requires the auth token to authenticate."
|
|
3309
|
+
},
|
|
3310
|
+
{
|
|
3311
|
+
id: "SP-069",
|
|
3312
|
+
name: "Twilio Auth Token",
|
|
3313
|
+
provider: "twilio",
|
|
3314
|
+
severity: "critical",
|
|
3315
|
+
pattern: /(?:TWILIO_AUTH_TOKEN|twilio[_-]auth[_-]token)['":\s=]+([a-fA-F0-9]{32})/i,
|
|
3316
|
+
description: "Twilio auth token. Combined with Account SID grants full access to messaging, phone numbers, and billing.",
|
|
3317
|
+
recommendation: "Rotate at console.twilio.com. Enable API key authentication instead of master auth token."
|
|
3318
|
+
},
|
|
3319
|
+
{
|
|
3320
|
+
id: "SP-070",
|
|
3321
|
+
name: "Twilio API Key Secret",
|
|
3322
|
+
provider: "twilio",
|
|
3323
|
+
severity: "high",
|
|
3324
|
+
pattern: /(?:SK[a-fA-F0-9]{32})/,
|
|
3325
|
+
description: "Twilio API Key SID (more restricted than auth token, still sensitive).",
|
|
3326
|
+
recommendation: "Rotate at console.twilio.com/user/api-keys."
|
|
3327
|
+
},
|
|
3328
|
+
{
|
|
3329
|
+
id: "SP-071",
|
|
3330
|
+
name: "SendGrid API Key",
|
|
3331
|
+
provider: "sendgrid",
|
|
3332
|
+
severity: "high",
|
|
3333
|
+
pattern: /SG\.[A-Za-z0-9_-]{22,30}\.[A-Za-z0-9_-]{43,50}/,
|
|
3334
|
+
description: "SendGrid email API key. Grants ability to send transactional email at your expense.",
|
|
3335
|
+
recommendation: "Rotate at app.sendgrid.com/settings/api_keys."
|
|
3336
|
+
},
|
|
3337
|
+
{
|
|
3338
|
+
id: "SP-072",
|
|
3339
|
+
name: "Resend API Key",
|
|
3340
|
+
provider: "resend",
|
|
3341
|
+
severity: "high",
|
|
3342
|
+
pattern: /re_[A-Za-z0-9_]{36,40}/,
|
|
3343
|
+
description: "Resend transactional email API key.",
|
|
3344
|
+
recommendation: "Rotate at resend.com/api-keys."
|
|
3345
|
+
},
|
|
3346
|
+
{
|
|
3347
|
+
id: "SP-073",
|
|
3348
|
+
name: "Mailgun API Key",
|
|
3349
|
+
provider: "mailgun",
|
|
3350
|
+
severity: "high",
|
|
3351
|
+
pattern: /key-[a-fA-F0-9]{32}/,
|
|
3352
|
+
description: "Mailgun private API key for sending and receiving email.",
|
|
3353
|
+
recommendation: "Rotate at app.mailgun.com/app/account/security/api_keys."
|
|
3354
|
+
},
|
|
3355
|
+
{
|
|
3356
|
+
id: "SP-074",
|
|
3357
|
+
name: "Mailchimp API Key",
|
|
3358
|
+
provider: "mailchimp",
|
|
3359
|
+
severity: "high",
|
|
3360
|
+
pattern: /[0-9a-f]{32}-us\d{1,2}/,
|
|
3361
|
+
description: "Mailchimp API key. Grants access to mailing lists, campaigns, and subscriber data.",
|
|
3362
|
+
recommendation: "Rotate at account.mailchimp.com/settings/api-keys."
|
|
3363
|
+
},
|
|
3364
|
+
// ── Developer Tools ──
|
|
3365
|
+
{
|
|
3366
|
+
id: "SP-075",
|
|
3367
|
+
name: "GitHub Personal Access Token (Classic)",
|
|
3368
|
+
provider: "github",
|
|
3369
|
+
severity: "critical",
|
|
3370
|
+
pattern: /ghp_[A-Za-z0-9]{36}/,
|
|
3371
|
+
description: "GitHub classic personal access token. Scope depends on permissions granted when created.",
|
|
3372
|
+
recommendation: "Revoke at github.com/settings/tokens. GitHub automatically detects and alerts on these."
|
|
3373
|
+
},
|
|
3374
|
+
{
|
|
3375
|
+
id: "SP-076",
|
|
3376
|
+
name: "GitHub Fine-Grained Token",
|
|
3377
|
+
provider: "github",
|
|
3378
|
+
severity: "critical",
|
|
3379
|
+
pattern: /github_pat_[A-Za-z0-9_]{82}/,
|
|
3380
|
+
description: "GitHub fine-grained personal access token with repository-level permissions.",
|
|
3381
|
+
recommendation: "Revoke at github.com/settings/tokens."
|
|
3382
|
+
},
|
|
3383
|
+
{
|
|
3384
|
+
id: "SP-077",
|
|
3385
|
+
name: "GitHub OAuth App Token",
|
|
3386
|
+
provider: "github",
|
|
3387
|
+
severity: "high",
|
|
3388
|
+
pattern: /gho_[A-Za-z0-9]{36}/,
|
|
3389
|
+
description: "GitHub OAuth app token. Grants access as the user who authorized the app.",
|
|
3390
|
+
recommendation: "Revoke at github.com/settings/applications."
|
|
3391
|
+
},
|
|
3392
|
+
{
|
|
3393
|
+
id: "SP-078",
|
|
3394
|
+
name: "GitHub Actions Token",
|
|
3395
|
+
provider: "github",
|
|
3396
|
+
severity: "high",
|
|
3397
|
+
pattern: /ghs_[A-Za-z0-9]{36}/,
|
|
3398
|
+
description: "GitHub Actions installation token. Short-lived but should not appear in artifacts.",
|
|
3399
|
+
recommendation: "These expire within an hour but should not be logged or exposed in build artifacts."
|
|
3400
|
+
},
|
|
3401
|
+
{
|
|
3402
|
+
id: "SP-079",
|
|
3403
|
+
name: "GitHub App Private Key",
|
|
3404
|
+
provider: "github",
|
|
3405
|
+
severity: "critical",
|
|
3406
|
+
pattern: /ghv_[A-Za-z0-9]{36}/,
|
|
3407
|
+
description: "GitHub repository-scoped token from a GitHub App installation.",
|
|
3408
|
+
recommendation: "Revoke via GitHub App settings."
|
|
3409
|
+
},
|
|
3410
|
+
{
|
|
3411
|
+
id: "SP-080",
|
|
3412
|
+
name: "npm Publish Token",
|
|
3413
|
+
provider: "npm",
|
|
3414
|
+
severity: "critical",
|
|
3415
|
+
pattern: /npm_[A-Za-z0-9]{36}/,
|
|
3416
|
+
description: "npm publish access token. Grants ability to publish packages under your account \u2014 supply chain attack vector.",
|
|
3417
|
+
recommendation: "Revoke at npmjs.com/settings/tokens. This is the highest risk secret for package maintainers."
|
|
3418
|
+
},
|
|
3419
|
+
{
|
|
3420
|
+
id: "SP-081",
|
|
3421
|
+
name: "PyPI API Token",
|
|
3422
|
+
provider: "pypi",
|
|
3423
|
+
severity: "critical",
|
|
3424
|
+
pattern: /pypi-[A-Za-z0-9_-]{32,80}/,
|
|
3425
|
+
description: "PyPI package publish token. Grants ability to publish Python packages \u2014 supply chain attack vector.",
|
|
3426
|
+
recommendation: "Revoke at pypi.org/manage/account/token. Use scoped tokens per-project."
|
|
3427
|
+
},
|
|
3428
|
+
// ── Databases ──
|
|
3429
|
+
{
|
|
3430
|
+
id: "SP-082",
|
|
3431
|
+
name: "PostgreSQL Connection String",
|
|
3432
|
+
provider: "database",
|
|
3433
|
+
severity: "critical",
|
|
3434
|
+
pattern: /postgres(?:ql)?:\/\/[^:@\s]+:[^@\s]+@[^/\s]+\/[^\s"']+/i,
|
|
3435
|
+
description: "PostgreSQL connection string with embedded credentials.",
|
|
3436
|
+
recommendation: "Rotate database credentials. Use connection poolers (PgBouncer, Supabase Pooler) and secrets managers."
|
|
3437
|
+
},
|
|
3438
|
+
{
|
|
3439
|
+
id: "SP-083",
|
|
3440
|
+
name: "MySQL Connection String",
|
|
3441
|
+
provider: "database",
|
|
3442
|
+
severity: "critical",
|
|
3443
|
+
pattern: /mysql:\/\/[^:@\s]+:[^@\s]+@[^/\s]+\/[^\s"']+/i,
|
|
3444
|
+
description: "MySQL connection string with embedded credentials.",
|
|
3445
|
+
recommendation: "Rotate credentials. Never include database URLs in client code."
|
|
3446
|
+
},
|
|
3447
|
+
{
|
|
3448
|
+
id: "SP-084",
|
|
3449
|
+
name: "MongoDB Connection String",
|
|
3450
|
+
provider: "database",
|
|
3451
|
+
severity: "critical",
|
|
3452
|
+
pattern: /mongodb(?:\+srv)?:\/\/[^:@\s]+:[^@\s]+@[^\s"']+/i,
|
|
3453
|
+
description: "MongoDB connection string with embedded credentials.",
|
|
3454
|
+
recommendation: "Rotate credentials. Enable IP allowlisting. Use MongoDB Atlas connection string with IAM auth."
|
|
3455
|
+
},
|
|
3456
|
+
{
|
|
3457
|
+
id: "SP-085",
|
|
3458
|
+
name: "Redis Connection String",
|
|
3459
|
+
provider: "database",
|
|
3460
|
+
severity: "high",
|
|
3461
|
+
pattern: /redis(?:s)?:\/\/(?:[^:@\s]+:[^@\s]+@)?[^/\s]+(?::\d+)?/i,
|
|
3462
|
+
description: "Redis connection string, potentially with authentication credentials.",
|
|
3463
|
+
recommendation: "Enable Redis AUTH. Use TLS (rediss://). Do not expose Redis to the public internet."
|
|
3464
|
+
},
|
|
3465
|
+
{
|
|
3466
|
+
id: "SP-086",
|
|
3467
|
+
name: "PlanetScale Service Token",
|
|
3468
|
+
provider: "planetscale",
|
|
3469
|
+
severity: "critical",
|
|
3470
|
+
pattern: /pscale_tkn_[A-Za-z0-9_]{32,50}/,
|
|
3471
|
+
description: "PlanetScale database service token.",
|
|
3472
|
+
recommendation: "Rotate at app.planetscale.com/settings/service-tokens."
|
|
3473
|
+
},
|
|
3474
|
+
{
|
|
3475
|
+
id: "SP-087",
|
|
3476
|
+
name: "Neon Database URL",
|
|
3477
|
+
provider: "neon",
|
|
3478
|
+
severity: "critical",
|
|
3479
|
+
pattern: /postgresql:\/\/[^:]+:[^@]+@[a-z0-9-]+\.(?:us-east-\d|eu-central-\d)\.aws\.neon\.tech\/[^\s"']+/,
|
|
3480
|
+
description: "Neon serverless PostgreSQL connection URL with credentials.",
|
|
3481
|
+
recommendation: "Rotate credentials at console.neon.tech. Use connection pooling endpoint."
|
|
3482
|
+
},
|
|
3483
|
+
{
|
|
3484
|
+
id: "SP-088",
|
|
3485
|
+
name: "Turso Database URL",
|
|
3486
|
+
provider: "turso",
|
|
3487
|
+
severity: "high",
|
|
3488
|
+
pattern: /libsql:\/\/[a-z0-9-]+-[a-z0-9]+\.turso\.io/,
|
|
3489
|
+
description: "Turso edge SQLite database connection URL.",
|
|
3490
|
+
recommendation: "Combine with auth token check. Rotate at app.turso.tech."
|
|
3491
|
+
},
|
|
3492
|
+
{
|
|
3493
|
+
id: "SP-089",
|
|
3494
|
+
name: "Turso Auth Token",
|
|
3495
|
+
provider: "turso",
|
|
3496
|
+
severity: "high",
|
|
3497
|
+
pattern: /(?:TURSO_AUTH_TOKEN|turso[_-]token)['":\s=]+([A-Za-z0-9_-]{100,200})/i,
|
|
3498
|
+
description: "Turso database auth token.",
|
|
3499
|
+
recommendation: "Rotate at app.turso.tech/databases."
|
|
3500
|
+
},
|
|
3501
|
+
// ── Analytics / Monitoring ──
|
|
3502
|
+
{
|
|
3503
|
+
id: "SP-090",
|
|
3504
|
+
name: "Sentry DSN",
|
|
3505
|
+
provider: "sentry",
|
|
3506
|
+
severity: "low",
|
|
3507
|
+
pattern: /https:\/\/[a-fA-F0-9]{32}@[a-z0-9]+\.ingest\.sentry\.io\/[0-9]+/,
|
|
3508
|
+
description: "Sentry Data Source Name. Can receive arbitrary error events and expose error data to unauthorized parties.",
|
|
3509
|
+
recommendation: "Rate-limit the DSN. Consider using Sentry Security Headers to restrict event submission."
|
|
3510
|
+
},
|
|
3511
|
+
{
|
|
3512
|
+
id: "SP-091",
|
|
3513
|
+
name: "Datadog API Key",
|
|
3514
|
+
provider: "datadog",
|
|
3515
|
+
severity: "high",
|
|
3516
|
+
pattern: /(?:DD_API_KEY|DATADOG_API_KEY)['":\s=]+([a-fA-F0-9]{32})/i,
|
|
3517
|
+
description: "Datadog API key for submitting metrics, logs, and events.",
|
|
3518
|
+
recommendation: "Rotate at app.datadoghq.com/organization-settings/api-keys."
|
|
3519
|
+
},
|
|
3520
|
+
{
|
|
3521
|
+
id: "SP-092",
|
|
3522
|
+
name: "Datadog App Key",
|
|
3523
|
+
provider: "datadog",
|
|
3524
|
+
severity: "high",
|
|
3525
|
+
pattern: /(?:DD_APP_KEY|DATADOG_APP_KEY)['":\s=]+([a-fA-F0-9]{40})/i,
|
|
3526
|
+
description: "Datadog application key for querying and managing Datadog resources.",
|
|
3527
|
+
recommendation: "Rotate at app.datadoghq.com/organization-settings/application-keys."
|
|
3528
|
+
},
|
|
3529
|
+
{
|
|
3530
|
+
id: "SP-093",
|
|
3531
|
+
name: "New Relic License Key",
|
|
3532
|
+
provider: "newrelic",
|
|
3533
|
+
severity: "high",
|
|
3534
|
+
pattern: /(?:NEW_RELIC_LICENSE_KEY|NRLIC)['":\s=]+([A-Za-z0-9]{40})/i,
|
|
3535
|
+
description: "New Relic ingest license key for APM, logs, and infrastructure.",
|
|
3536
|
+
recommendation: "Rotate at one.newrelic.com/api-keys."
|
|
3537
|
+
},
|
|
3538
|
+
{
|
|
3539
|
+
id: "SP-094",
|
|
3540
|
+
name: "PostHog API Key",
|
|
3541
|
+
provider: "posthog",
|
|
3542
|
+
severity: "medium",
|
|
3543
|
+
pattern: /phc_[A-Za-z0-9]{43}/,
|
|
3544
|
+
description: "PostHog project API key. Client-side analytics key \u2014 can be semi-public but controls data ingest.",
|
|
3545
|
+
recommendation: "This key is typically safe on the client, but ensure project-level access controls are configured."
|
|
3546
|
+
},
|
|
3547
|
+
{
|
|
3548
|
+
id: "SP-095",
|
|
3549
|
+
name: "Amplitude API Key",
|
|
3550
|
+
provider: "amplitude",
|
|
3551
|
+
severity: "medium",
|
|
3552
|
+
pattern: /(?:AMPLITUDE_API_KEY|amplitude[_-]api[_-]key)['":\s=]+([a-fA-F0-9]{32})/i,
|
|
3553
|
+
description: "Amplitude analytics project API key.",
|
|
3554
|
+
recommendation: "Low risk for the API key, but the secret key should be server-side only."
|
|
3555
|
+
},
|
|
3556
|
+
{
|
|
3557
|
+
id: "SP-096",
|
|
3558
|
+
name: "Mixpanel Token",
|
|
3559
|
+
provider: "mixpanel",
|
|
3560
|
+
severity: "low",
|
|
3561
|
+
pattern: /(?:MIXPANEL_TOKEN|NEXT_PUBLIC_MIXPANEL)['":\s=]+([a-fA-F0-9]{32})/i,
|
|
3562
|
+
description: "Mixpanel analytics project token. Designed to be public for event tracking.",
|
|
3563
|
+
recommendation: "Project token is safe to be public. Keep the project secret server-side."
|
|
3564
|
+
},
|
|
3565
|
+
{
|
|
3566
|
+
id: "SP-097",
|
|
3567
|
+
name: "Segment Write Key",
|
|
3568
|
+
provider: "segment",
|
|
3569
|
+
severity: "medium",
|
|
3570
|
+
pattern: /(?:SEGMENT_WRITE_KEY|analytics\.load\()['":\s(]+([A-Za-z0-9]{20,30})/i,
|
|
3571
|
+
description: "Segment analytics write key. Allows sending arbitrary events to your workspace.",
|
|
3572
|
+
recommendation: "Client-side write keys are semi-public, but server write keys should be kept private."
|
|
3573
|
+
},
|
|
3574
|
+
// ── Search and Data ──
|
|
3575
|
+
{
|
|
3576
|
+
id: "SP-098",
|
|
3577
|
+
name: "Algolia App ID & API Key",
|
|
3578
|
+
provider: "algolia",
|
|
3579
|
+
severity: "medium",
|
|
3580
|
+
pattern: /(?:ALGOLIA_API_KEY|NEXT_PUBLIC_ALGOLIA_API_KEY)['":\s=]+([A-Za-z0-9]{32})/i,
|
|
3581
|
+
description: "Algolia search API key. Admin key grants full index management; search-only key is safe to expose.",
|
|
3582
|
+
recommendation: "Use search-only API keys on the client. Rotate admin keys at algolia.com/account/api-keys."
|
|
3583
|
+
},
|
|
3584
|
+
{
|
|
3585
|
+
id: "SP-099",
|
|
3586
|
+
name: "Algolia Admin API Key",
|
|
3587
|
+
provider: "algolia",
|
|
3588
|
+
severity: "critical",
|
|
3589
|
+
pattern: /(?:ALGOLIA_ADMIN_API_KEY)['":\s=]+([A-Za-z0-9]{32})/i,
|
|
3590
|
+
description: "Algolia admin API key. Grants full index management including delete operations.",
|
|
3591
|
+
recommendation: "Never expose admin key on client. Rotate at algolia.com/account/api-keys."
|
|
3592
|
+
},
|
|
3593
|
+
{
|
|
3594
|
+
id: "SP-100",
|
|
3595
|
+
name: "Mapbox Access Token",
|
|
3596
|
+
provider: "mapbox",
|
|
3597
|
+
severity: "medium",
|
|
3598
|
+
pattern: /pk\.[a-zA-Z0-9.]+\.[a-zA-Z0-9_-]+/,
|
|
3599
|
+
description: "Mapbox public access token. Designed for client use but can be abused for quota theft.",
|
|
3600
|
+
recommendation: "Restrict token by URL in Mapbox account settings."
|
|
3601
|
+
},
|
|
3602
|
+
// ── Cryptographic / Generic ──
|
|
3603
|
+
{
|
|
3604
|
+
id: "SP-101",
|
|
3605
|
+
name: "RSA Private Key",
|
|
3606
|
+
provider: "cryptographic",
|
|
3607
|
+
severity: "critical",
|
|
3608
|
+
pattern: /-----BEGIN RSA PRIVATE KEY-----/,
|
|
3609
|
+
description: "RSA private key embedded in source. Can be used for decryption or identity impersonation.",
|
|
3610
|
+
recommendation: "Remove immediately. Use secrets managers (AWS Secrets Manager, Vault, Doppler)."
|
|
3611
|
+
},
|
|
3612
|
+
{
|
|
3613
|
+
id: "SP-102",
|
|
3614
|
+
name: "EC Private Key",
|
|
3615
|
+
provider: "cryptographic",
|
|
3616
|
+
severity: "critical",
|
|
3617
|
+
pattern: /-----BEGIN EC PRIVATE KEY-----/,
|
|
3618
|
+
description: "Elliptic curve private key embedded in source.",
|
|
3619
|
+
recommendation: "Remove immediately. Rotate the key pair and audit for any usage."
|
|
3620
|
+
},
|
|
3621
|
+
{
|
|
3622
|
+
id: "SP-103",
|
|
3623
|
+
name: "PKCS8 Private Key",
|
|
3624
|
+
provider: "cryptographic",
|
|
3625
|
+
severity: "critical",
|
|
3626
|
+
pattern: /-----BEGIN PRIVATE KEY-----/,
|
|
3627
|
+
description: "PKCS#8 private key in unencrypted PEM format.",
|
|
3628
|
+
recommendation: "Remove immediately. Use environment variables with proper secret management."
|
|
3629
|
+
},
|
|
3630
|
+
{
|
|
3631
|
+
id: "SP-104",
|
|
3632
|
+
name: "PGP Private Key",
|
|
3633
|
+
provider: "cryptographic",
|
|
3634
|
+
severity: "critical",
|
|
3635
|
+
pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----/,
|
|
3636
|
+
description: "PGP/GPG private key embedded in source.",
|
|
3637
|
+
recommendation: "Remove and revoke the key at your keyserver."
|
|
3638
|
+
},
|
|
3639
|
+
{
|
|
3640
|
+
id: "SP-105",
|
|
3641
|
+
name: "Generic JWT Secret",
|
|
3642
|
+
provider: "generic",
|
|
3643
|
+
severity: "high",
|
|
3644
|
+
pattern: /(?:JWT_SECRET|jwt[_-]secret|JWT_SIGNING_KEY)['":\s=]+["']?([A-Za-z0-9_+/=-]{32,})/i,
|
|
3645
|
+
description: "JWT signing secret. Anyone with this secret can forge valid authentication tokens for your application.",
|
|
3646
|
+
recommendation: "Rotate the secret and invalidate all existing tokens. Use asymmetric keys (RS256/ES256) instead of symmetric."
|
|
3647
|
+
},
|
|
3648
|
+
{
|
|
3649
|
+
id: "SP-106",
|
|
3650
|
+
name: "Generic API Secret",
|
|
3651
|
+
provider: "generic",
|
|
3652
|
+
severity: "high",
|
|
3653
|
+
pattern: /(?:API_SECRET|app[_-]secret|CLIENT_SECRET)['":\s=]+["']?([A-Za-z0-9_+/=.-]{32,})/i,
|
|
3654
|
+
description: "Generic application secret or client secret.",
|
|
3655
|
+
recommendation: "Audit the usage of this secret and rotate if exposed."
|
|
3656
|
+
},
|
|
3657
|
+
{
|
|
3658
|
+
id: "SP-107",
|
|
3659
|
+
name: "Generic Webhook Secret",
|
|
3660
|
+
provider: "generic",
|
|
3661
|
+
severity: "high",
|
|
3662
|
+
pattern: /(?:WEBHOOK_SECRET|webhook[_-]signing[_-]key)['":\s=]+["']?([A-Za-z0-9_+/=-]{32,})/i,
|
|
3663
|
+
description: "Webhook signing secret. Allows forging webhook payloads from third-party services.",
|
|
3664
|
+
recommendation: "Rotate the webhook secret at the originating service."
|
|
3665
|
+
},
|
|
3666
|
+
// ── Social Platforms ──
|
|
3667
|
+
{
|
|
3668
|
+
id: "SP-108",
|
|
3669
|
+
name: "Twitter/X Bearer Token",
|
|
3670
|
+
provider: "twitter",
|
|
3671
|
+
severity: "high",
|
|
3672
|
+
pattern: /AAAAAAAAAAAAAAAAAAAAAA[A-Za-z0-9%]+/,
|
|
3673
|
+
description: "Twitter/X API v2 bearer token. Grants read access to public Twitter data.",
|
|
3674
|
+
recommendation: "Rotate at developer.twitter.com/en/apps."
|
|
3675
|
+
},
|
|
3676
|
+
{
|
|
3677
|
+
id: "SP-109",
|
|
3678
|
+
name: "Twitter API Key",
|
|
3679
|
+
provider: "twitter",
|
|
3680
|
+
severity: "high",
|
|
3681
|
+
pattern: /(?:TWITTER_API_KEY|TWITTER_CONSUMER_KEY)['":\s=]+([A-Za-z0-9]{25})/i,
|
|
3682
|
+
description: "Twitter/X API consumer key.",
|
|
3683
|
+
recommendation: "Rotate at developer.twitter.com/en/apps."
|
|
3684
|
+
},
|
|
3685
|
+
{
|
|
3686
|
+
id: "SP-110",
|
|
3687
|
+
name: "Twitter API Secret",
|
|
3688
|
+
provider: "twitter",
|
|
3689
|
+
severity: "critical",
|
|
3690
|
+
pattern: /(?:TWITTER_API_SECRET|TWITTER_CONSUMER_SECRET)['":\s=]+([A-Za-z0-9]{50})/i,
|
|
3691
|
+
description: "Twitter/X API consumer secret. Combined with key grants OAuth application access.",
|
|
3692
|
+
recommendation: "Rotate at developer.twitter.com/en/apps. Required to regenerate if compromised."
|
|
3693
|
+
},
|
|
3694
|
+
{
|
|
3695
|
+
id: "SP-111",
|
|
3696
|
+
name: "HubSpot Private App Token",
|
|
3697
|
+
provider: "hubspot",
|
|
3698
|
+
severity: "high",
|
|
3699
|
+
pattern: /pat-(?:na|eu)\d-[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/,
|
|
3700
|
+
description: "HubSpot private app access token. Grants CRM data access.",
|
|
3701
|
+
recommendation: "Rotate at app.hubspot.com/private-apps."
|
|
3702
|
+
},
|
|
3703
|
+
{
|
|
3704
|
+
id: "SP-112",
|
|
3705
|
+
name: "Notion Integration Token",
|
|
3706
|
+
provider: "notion",
|
|
3707
|
+
severity: "high",
|
|
3708
|
+
pattern: /secret_[A-Za-z0-9]{43}/,
|
|
3709
|
+
description: "Notion internal integration token. Grants access to all pages shared with the integration.",
|
|
3710
|
+
recommendation: "Rotate at notion.so/my-integrations."
|
|
3711
|
+
},
|
|
3712
|
+
{
|
|
3713
|
+
id: "SP-113",
|
|
3714
|
+
name: "Airtable API Key",
|
|
3715
|
+
provider: "airtable",
|
|
3716
|
+
severity: "high",
|
|
3717
|
+
pattern: /(?:AIRTABLE_API_KEY)['":\s=]+([A-Za-z0-9]{17})/i,
|
|
3718
|
+
description: "Airtable personal API key. Grants access to all bases the account can access.",
|
|
3719
|
+
recommendation: "Rotate at airtable.com/account. Prefer scoped personal access tokens."
|
|
3720
|
+
},
|
|
3721
|
+
{
|
|
3722
|
+
id: "SP-114",
|
|
3723
|
+
name: "Airtable Personal Access Token",
|
|
3724
|
+
provider: "airtable",
|
|
3725
|
+
severity: "high",
|
|
3726
|
+
pattern: /pat[A-Za-z0-9]{14}\.[a-fA-F0-9]{64}/,
|
|
3727
|
+
description: "Airtable scoped personal access token.",
|
|
3728
|
+
recommendation: "Rotate at airtable.com/create/tokens."
|
|
3729
|
+
},
|
|
3730
|
+
{
|
|
3731
|
+
id: "SP-115",
|
|
3732
|
+
name: "Linear API Key",
|
|
3733
|
+
provider: "linear",
|
|
3734
|
+
severity: "high",
|
|
3735
|
+
pattern: /lin_api_[A-Za-z0-9]{40}/,
|
|
3736
|
+
description: "Linear project management API key. Grants access to issues, projects, and teams.",
|
|
3737
|
+
recommendation: "Rotate at linear.app/settings/api."
|
|
3738
|
+
},
|
|
3739
|
+
// ── Infrastructure / DevOps ──
|
|
3740
|
+
{
|
|
3741
|
+
id: "SP-116",
|
|
3742
|
+
name: "HashiCorp Vault Token",
|
|
3743
|
+
provider: "hashicorp",
|
|
3744
|
+
severity: "critical",
|
|
3745
|
+
pattern: /(?:VAULT_TOKEN|hvs\.[A-Za-z0-9]+)/,
|
|
3746
|
+
description: "HashiCorp Vault service token. Grants access to secrets stored in Vault.",
|
|
3747
|
+
recommendation: "Revoke with vault token revoke. Enable token TTLs and audit logging."
|
|
3748
|
+
},
|
|
3749
|
+
{
|
|
3750
|
+
id: "SP-117",
|
|
3751
|
+
name: "Kubernetes Service Account Token",
|
|
3752
|
+
provider: "kubernetes",
|
|
3753
|
+
severity: "critical",
|
|
3754
|
+
pattern: /eyJhbGciOiJSUzI1NiIsImtpZCI[A-Za-z0-9_-]+\.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50/,
|
|
3755
|
+
description: "Kubernetes service account JWT token. Grants API server access within the cluster.",
|
|
3756
|
+
recommendation: "Rotate the service account token. Use short-lived projected tokens (TokenRequest API)."
|
|
3757
|
+
},
|
|
3758
|
+
{
|
|
3759
|
+
id: "SP-118",
|
|
3760
|
+
name: "Cloudinary URL",
|
|
3761
|
+
provider: "cloudinary",
|
|
3762
|
+
severity: "high",
|
|
3763
|
+
pattern: /cloudinary:\/\/[0-9]+:[A-Za-z0-9_-]+@[a-z0-9_]+/,
|
|
3764
|
+
description: "Cloudinary media storage URL with API credentials.",
|
|
3765
|
+
recommendation: "Rotate at cloudinary.com/console. This grants upload and transformation access."
|
|
3766
|
+
},
|
|
3767
|
+
{
|
|
3768
|
+
id: "SP-119",
|
|
3769
|
+
name: "Upstash Redis Token",
|
|
3770
|
+
provider: "upstash",
|
|
3771
|
+
severity: "high",
|
|
3772
|
+
pattern: /(?:UPSTASH_REDIS_REST_TOKEN|upstash[_-]token)['":\s=]+([A-Za-z0-9_-]{50,100})/i,
|
|
3773
|
+
description: "Upstash serverless Redis REST API token.",
|
|
3774
|
+
recommendation: "Rotate at console.upstash.com."
|
|
3775
|
+
},
|
|
3776
|
+
{
|
|
3777
|
+
id: "SP-120",
|
|
3778
|
+
name: "Upstash QStash Token",
|
|
3779
|
+
provider: "upstash",
|
|
3780
|
+
severity: "high",
|
|
3781
|
+
pattern: /(?:QSTASH_TOKEN)['":\s=]+([A-Za-z0-9_-]{100,200})/i,
|
|
3782
|
+
description: "Upstash QStash serverless message queue token.",
|
|
3783
|
+
recommendation: "Rotate at console.upstash.com/qstash."
|
|
3784
|
+
},
|
|
3785
|
+
// ── Additional AI/ML Platforms ──
|
|
3786
|
+
{
|
|
3787
|
+
id: "SP-121",
|
|
3788
|
+
name: "Portkey API Key",
|
|
3789
|
+
provider: "portkey",
|
|
3790
|
+
severity: "medium",
|
|
3791
|
+
pattern: /(?:PORTKEY_API_KEY)['":\s=]+([A-Za-z0-9_-]{40,60})/i,
|
|
3792
|
+
description: "Portkey AI gateway API key.",
|
|
3793
|
+
recommendation: "Rotate at app.portkey.ai/settings."
|
|
3794
|
+
},
|
|
3795
|
+
{
|
|
3796
|
+
id: "SP-122",
|
|
3797
|
+
name: "Langfuse Secret Key",
|
|
3798
|
+
provider: "langfuse",
|
|
3799
|
+
severity: "medium",
|
|
3800
|
+
pattern: /sk-lf-[A-Za-z0-9_-]{36,50}/,
|
|
3801
|
+
description: "Langfuse LLM observability secret key.",
|
|
3802
|
+
recommendation: "Rotate at cloud.langfuse.com/project/settings."
|
|
3803
|
+
},
|
|
3804
|
+
{
|
|
3805
|
+
id: "SP-123",
|
|
3806
|
+
name: "Weaviate API Key",
|
|
3807
|
+
provider: "weaviate",
|
|
3808
|
+
severity: "high",
|
|
3809
|
+
pattern: /(?:WEAVIATE_API_KEY)['":\s=]+([A-Za-z0-9_-]{32,50})/i,
|
|
3810
|
+
description: "Weaviate vector database API key.",
|
|
3811
|
+
recommendation: "Rotate at the Weaviate Cloud Services console."
|
|
3812
|
+
},
|
|
3813
|
+
{
|
|
3814
|
+
id: "SP-124",
|
|
3815
|
+
name: "Pinecone API Key",
|
|
3816
|
+
provider: "pinecone",
|
|
3817
|
+
severity: "high",
|
|
3818
|
+
pattern: /(?:PINECONE_API_KEY)['":\s=]+([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})/i,
|
|
3819
|
+
description: "Pinecone vector database API key. Grants access to vector indices and stored embeddings.",
|
|
3820
|
+
recommendation: "Rotate at app.pinecone.io/organization/api-keys."
|
|
3821
|
+
},
|
|
3822
|
+
{
|
|
3823
|
+
id: "SP-125",
|
|
3824
|
+
name: "Browserbase API Key",
|
|
3825
|
+
provider: "browserbase",
|
|
3826
|
+
severity: "high",
|
|
3827
|
+
pattern: /(?:BROWSERBASE_API_KEY)['":\s=]+([A-Za-z0-9_-]{32,50})/i,
|
|
3828
|
+
description: "Browserbase cloud browser API key for web automation.",
|
|
3829
|
+
recommendation: "Rotate at browserbase.com/settings."
|
|
3830
|
+
},
|
|
3831
|
+
// ── Payments (extended) ──
|
|
3832
|
+
{
|
|
3833
|
+
id: "SP-126",
|
|
3834
|
+
name: "Braintree API Key",
|
|
3835
|
+
provider: "braintree",
|
|
3836
|
+
severity: "critical",
|
|
3837
|
+
pattern: /(?:BRAINTREE_PRIVATE_KEY|braintree[_-]private[_-]key)['":\s=]+([A-Za-z0-9]{32})/i,
|
|
3838
|
+
description: "Braintree payment processing private key.",
|
|
3839
|
+
recommendation: "Rotate at sandbox.braintreegateway.com or braintreegateway.com."
|
|
3840
|
+
},
|
|
3841
|
+
// ── Communication (extended) ──
|
|
3842
|
+
{
|
|
3843
|
+
id: "SP-127",
|
|
3844
|
+
name: "Pusher App Secret",
|
|
3845
|
+
provider: "pusher",
|
|
3846
|
+
severity: "high",
|
|
3847
|
+
pattern: /(?:PUSHER_APP_SECRET)['":\s=]+([a-fA-F0-9]{20})/i,
|
|
3848
|
+
description: "Pusher Channels app secret for server-side event authentication.",
|
|
3849
|
+
recommendation: "Rotate at dashboard.pusher.com."
|
|
3850
|
+
},
|
|
3851
|
+
{
|
|
3852
|
+
id: "SP-128",
|
|
3853
|
+
name: "Ably API Key",
|
|
3854
|
+
provider: "ably",
|
|
3855
|
+
severity: "high",
|
|
3856
|
+
pattern: /[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+:[A-Za-z0-9_+/=]{32,50}/,
|
|
3857
|
+
description: "Ably realtime messaging API key.",
|
|
3858
|
+
recommendation: "Rotate at ably.com/accounts."
|
|
3859
|
+
},
|
|
3860
|
+
{
|
|
3861
|
+
id: "SP-129",
|
|
3862
|
+
name: "Zoom JWT Token",
|
|
3863
|
+
provider: "zoom",
|
|
3864
|
+
severity: "high",
|
|
3865
|
+
pattern: /(?:ZOOM_JWT_TOKEN|zoom[_-]jwt)['":\s=]+([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/i,
|
|
3866
|
+
description: "Zoom JWT application token for Server-to-Server OAuth.",
|
|
3867
|
+
recommendation: "Rotate at marketplace.zoom.us/develop/apps."
|
|
3868
|
+
},
|
|
3869
|
+
// ── Data Platforms ──
|
|
3870
|
+
{
|
|
3871
|
+
id: "SP-130",
|
|
3872
|
+
name: "Elastic Search API Key",
|
|
3873
|
+
provider: "elastic",
|
|
3874
|
+
severity: "high",
|
|
3875
|
+
pattern: /(?:ELASTICSEARCH_API_KEY|elastic[_-]api[_-]key)['":\s=]+([A-Za-z0-9_=]+:{[^}]+})/i,
|
|
3876
|
+
description: "Elasticsearch / Elastic Cloud API key.",
|
|
3877
|
+
recommendation: "Rotate in Kibana > Stack Management > Security > API Keys."
|
|
3878
|
+
},
|
|
3879
|
+
{
|
|
3880
|
+
id: "SP-131",
|
|
3881
|
+
name: "Kafka SASL Password",
|
|
3882
|
+
provider: "kafka",
|
|
3883
|
+
severity: "critical",
|
|
3884
|
+
pattern: /(?:KAFKA_SASL_PASSWORD|kafka[_-]password)['":\s=]+([^\s"']{8,})/i,
|
|
3885
|
+
description: "Apache Kafka SASL authentication password.",
|
|
3886
|
+
recommendation: "Rotate and enable SSL/TLS for Kafka connections."
|
|
3887
|
+
},
|
|
3888
|
+
// ── Observability (extended) ──
|
|
3889
|
+
{
|
|
3890
|
+
id: "SP-132",
|
|
3891
|
+
name: "Axiom API Token",
|
|
3892
|
+
provider: "axiom",
|
|
3893
|
+
severity: "medium",
|
|
3894
|
+
pattern: /xaat-[A-Za-z0-9_-]{36,50}/,
|
|
3895
|
+
description: "Axiom log analytics API token.",
|
|
3896
|
+
recommendation: "Rotate at app.axiom.co/settings/api-tokens."
|
|
3897
|
+
},
|
|
3898
|
+
{
|
|
3899
|
+
id: "SP-133",
|
|
3900
|
+
name: "Grafana API Key",
|
|
3901
|
+
provider: "grafana",
|
|
3902
|
+
severity: "high",
|
|
3903
|
+
pattern: /(?:GRAFANA_API_KEY|grafana[_-]token)['":\s=]+([A-Za-z0-9=]{40,100})/i,
|
|
3904
|
+
description: "Grafana Cloud API key for metrics, logs, and traces.",
|
|
3905
|
+
recommendation: "Rotate at grafana.com/profile/api-keys."
|
|
3906
|
+
},
|
|
3907
|
+
// ── Environment Variable Leaks ──
|
|
3908
|
+
{
|
|
3909
|
+
id: "SP-134",
|
|
3910
|
+
name: "Exposed NEXT_PUBLIC Secret Variable",
|
|
3911
|
+
provider: "nextjs",
|
|
3912
|
+
severity: "high",
|
|
3913
|
+
pattern: /NEXT_PUBLIC_(?:SECRET|KEY|TOKEN|PASSWORD|PRIVATE)[A-Z_]*['":\s=]+["']?[A-Za-z0-9_+/=-]{20,}/i,
|
|
3914
|
+
description: "Variable prefixed NEXT_PUBLIC_ (client-bundle exposure) with a name suggesting it is a secret. NEXT_PUBLIC_ variables are always bundled into the client.",
|
|
3915
|
+
recommendation: "Remove NEXT_PUBLIC_ prefix. Move to server-side environment variables or use Next.js server actions."
|
|
3916
|
+
},
|
|
3917
|
+
{
|
|
3918
|
+
id: "SP-135",
|
|
3919
|
+
name: "Exposed VITE Secret Variable",
|
|
3920
|
+
provider: "vite",
|
|
3921
|
+
severity: "high",
|
|
3922
|
+
pattern: /VITE_(?:SECRET|KEY|TOKEN|PASSWORD|PRIVATE)[A-Z_]*['":\s=]+["']?[A-Za-z0-9_+/=-]{20,}/i,
|
|
3923
|
+
description: "Variable prefixed VITE_ (client-bundle exposure) with a name suggesting it is a secret. VITE_ variables are always bundled into the client.",
|
|
3924
|
+
recommendation: "Remove VITE_ prefix and handle server-side. Use API routes for sensitive operations."
|
|
3925
|
+
},
|
|
3926
|
+
{
|
|
3927
|
+
id: "SP-136",
|
|
3928
|
+
name: "Hardcoded Password Pattern",
|
|
3929
|
+
provider: "generic",
|
|
3930
|
+
severity: "high",
|
|
3931
|
+
pattern: /(?:password|passwd|db_pass|db_password)['":\s=]+["'](?!.*\${)[A-Za-z0-9!@#$%^&*_+=]{8,}/i,
|
|
3932
|
+
description: "Hardcoded password string that does not appear to be a template variable.",
|
|
3933
|
+
recommendation: "Move credentials to environment variables and use a secrets manager."
|
|
3934
|
+
},
|
|
3935
|
+
// ── AI Agent Ecosystem ──
|
|
3936
|
+
{
|
|
3937
|
+
id: "SP-137",
|
|
3938
|
+
name: "Smithery Registry Token",
|
|
3939
|
+
provider: "smithery",
|
|
3940
|
+
severity: "high",
|
|
3941
|
+
pattern: /(?:SMITHERY_API_KEY|smithery[_-]token)['":\s=]+([A-Za-z0-9_-]{30,60})/i,
|
|
3942
|
+
description: "Smithery MCP registry API token. Grants publish access to MCP server packages.",
|
|
3943
|
+
recommendation: "Rotate at smithery.ai/settings. Supply chain risk \u2014 protect publish access."
|
|
3944
|
+
},
|
|
3945
|
+
{
|
|
3946
|
+
id: "SP-138",
|
|
3947
|
+
name: "Cursor API Key",
|
|
3948
|
+
provider: "cursor",
|
|
3949
|
+
severity: "medium",
|
|
3950
|
+
pattern: /(?:CURSOR_API_KEY)['":\s=]+([A-Za-z0-9_-]{30,60})/i,
|
|
3951
|
+
description: "Cursor IDE API integration key.",
|
|
3952
|
+
recommendation: "Rotate at cursor.com/settings."
|
|
3953
|
+
},
|
|
3954
|
+
{
|
|
3955
|
+
id: "SP-139",
|
|
3956
|
+
name: "Vigile API Key",
|
|
3957
|
+
provider: "vigile",
|
|
3958
|
+
severity: "medium",
|
|
3959
|
+
pattern: /vgl_[A-Za-z0-9_-]{40,60}/,
|
|
3960
|
+
description: "Vigile AI API key. Grants access to Vigile scan API and trust registry.",
|
|
3961
|
+
recommendation: "Rotate at vigile.dev/account."
|
|
3962
|
+
},
|
|
3963
|
+
{
|
|
3964
|
+
id: "SP-140",
|
|
3965
|
+
name: "OpenAI API Key (via env)",
|
|
3966
|
+
provider: "openai",
|
|
3967
|
+
severity: "critical",
|
|
3968
|
+
pattern: /(?:OPENAI_API_KEY)['":\s=]+["']?(sk-[A-Za-z0-9_-]{48,})/i,
|
|
3969
|
+
description: "OpenAI API key referenced in an environment variable assignment.",
|
|
3970
|
+
recommendation: "Rotate at platform.openai.com/api-keys. Move to server-side proxy."
|
|
3971
|
+
},
|
|
3972
|
+
{
|
|
3973
|
+
id: "SP-141",
|
|
3974
|
+
name: "Anthropic API Key (via env)",
|
|
3975
|
+
provider: "anthropic",
|
|
3976
|
+
severity: "critical",
|
|
3977
|
+
pattern: /(?:ANTHROPIC_API_KEY)['":\s=]+["']?(sk-ant-api[A-Za-z0-9_-]{90,})/i,
|
|
3978
|
+
description: "Anthropic API key in an environment variable assignment.",
|
|
3979
|
+
recommendation: "Rotate at console.anthropic.com. Never include in client bundles."
|
|
3980
|
+
},
|
|
3981
|
+
// ── Extended Coverage ──
|
|
3982
|
+
{
|
|
3983
|
+
id: "SP-142",
|
|
3984
|
+
name: "Stripe Secret Key (Test)",
|
|
3985
|
+
provider: "stripe",
|
|
3986
|
+
severity: "medium",
|
|
3987
|
+
pattern: /sk_test_[A-Za-z0-9]{24,100}/,
|
|
3988
|
+
description: "Stripe test-mode secret key. Should not be in production bundles.",
|
|
3989
|
+
recommendation: "Test keys should not appear in client code even in development builds."
|
|
3990
|
+
},
|
|
3991
|
+
{
|
|
3992
|
+
id: "SP-143",
|
|
3993
|
+
name: "GitHub App Installation Token",
|
|
3994
|
+
provider: "github",
|
|
3995
|
+
severity: "high",
|
|
3996
|
+
pattern: /ghi_[A-Za-z0-9]{36}/,
|
|
3997
|
+
description: "GitHub App installation token (internal use).",
|
|
3998
|
+
recommendation: "These are short-lived but should not appear in artifacts."
|
|
3999
|
+
},
|
|
4000
|
+
{
|
|
4001
|
+
id: "SP-144",
|
|
4002
|
+
name: "Supabase JWT Secret",
|
|
4003
|
+
provider: "supabase",
|
|
4004
|
+
severity: "critical",
|
|
4005
|
+
pattern: /(?:SUPABASE_JWT_SECRET|JWT_SECRET)['":\s=]+["']?[A-Za-z0-9_+/=-]{32,}/i,
|
|
4006
|
+
description: "JWT signing secret for Supabase Auth. Anyone with this secret can forge valid session tokens.",
|
|
4007
|
+
recommendation: "This secret is set at project creation and cannot be rotated without breaking all sessions."
|
|
4008
|
+
},
|
|
4009
|
+
{
|
|
4010
|
+
id: "SP-145",
|
|
4011
|
+
name: "Firebase Admin Private Key",
|
|
4012
|
+
provider: "firebase",
|
|
4013
|
+
severity: "critical",
|
|
4014
|
+
pattern: /(?:FIREBASE_PRIVATE_KEY)['":\s=]+["']?-----BEGIN (RSA )?PRIVATE KEY/i,
|
|
4015
|
+
description: "Firebase Admin SDK private key. Grants admin-level database and Auth access.",
|
|
4016
|
+
recommendation: "Remove from client code immediately. Firebase Admin SDK must only run server-side."
|
|
4017
|
+
},
|
|
4018
|
+
{
|
|
4019
|
+
id: "SP-146",
|
|
4020
|
+
name: "Google Analytics Measurement ID",
|
|
4021
|
+
provider: "google",
|
|
4022
|
+
severity: "low",
|
|
4023
|
+
pattern: /G-[A-Z0-9]{10}/,
|
|
4024
|
+
description: "Google Analytics 4 Measurement ID. Designed to be public \u2014 included for inventory purposes.",
|
|
4025
|
+
recommendation: "This is public by design but note it for compliance/privacy documentation."
|
|
4026
|
+
},
|
|
4027
|
+
{
|
|
4028
|
+
id: "SP-147",
|
|
4029
|
+
name: "Imagekit Private Key",
|
|
4030
|
+
provider: "imagekit",
|
|
4031
|
+
severity: "high",
|
|
4032
|
+
pattern: /(?:IMAGEKIT_PRIVATE_KEY)['":\s=]+([A-Za-z0-9_-]{30,60})/i,
|
|
4033
|
+
description: "Imagekit.io private API key for image upload and management.",
|
|
4034
|
+
recommendation: "Rotate at imagekit.io/dashboard/developer/api-keys."
|
|
4035
|
+
},
|
|
4036
|
+
{
|
|
4037
|
+
id: "SP-148",
|
|
4038
|
+
name: "ScraperAPI Key",
|
|
4039
|
+
provider: "scraperapi",
|
|
4040
|
+
severity: "medium",
|
|
4041
|
+
pattern: /(?:SCRAPERAPI_KEY|scraperapi[_-]key)['":\s=]+([a-fA-F0-9]{32})/i,
|
|
4042
|
+
description: "ScraperAPI proxy key for web scraping requests.",
|
|
4043
|
+
recommendation: "Rotate at scraperapi.com/dashboard."
|
|
4044
|
+
},
|
|
4045
|
+
{
|
|
4046
|
+
id: "SP-149",
|
|
4047
|
+
name: "Salesforce Connected App Secret",
|
|
4048
|
+
provider: "salesforce",
|
|
4049
|
+
severity: "critical",
|
|
4050
|
+
pattern: /(?:SF_CLIENT_SECRET|SALESFORCE_SECRET)['":\s=]+([A-Za-z0-9_-]{64})/i,
|
|
4051
|
+
description: "Salesforce Connected App client secret. Grants CRM access as the connected application.",
|
|
4052
|
+
recommendation: "Rotate in Salesforce Setup > App Manager."
|
|
4053
|
+
},
|
|
4054
|
+
{
|
|
4055
|
+
id: "SP-150",
|
|
4056
|
+
name: "Generic Bearer Token",
|
|
4057
|
+
provider: "generic",
|
|
4058
|
+
severity: "medium",
|
|
4059
|
+
pattern: /Authorization:\s*Bearer\s+([A-Za-z0-9_-]{40,200})/i,
|
|
4060
|
+
description: "Bearer token hardcoded in Authorization header \u2014 likely a live credential.",
|
|
4061
|
+
recommendation: "Remove hardcoded tokens. Generate tokens at runtime from securely stored credentials."
|
|
4062
|
+
}
|
|
4063
|
+
];
|
|
4064
|
+
function matchSecrets(text) {
|
|
4065
|
+
const results = [];
|
|
4066
|
+
for (const sp of SECRET_PATTERNS) {
|
|
4067
|
+
const flags = sp.pattern.flags.includes("i") ? "gi" : "g";
|
|
4068
|
+
const globalPattern = new RegExp(sp.pattern.source, flags);
|
|
4069
|
+
for (const m of text.matchAll(globalPattern)) {
|
|
4070
|
+
const matched = m[0];
|
|
4071
|
+
const idx = m.index ?? 0;
|
|
4072
|
+
const start = Math.max(0, idx - 25);
|
|
4073
|
+
const end = Math.min(text.length, idx + matched.length + 25);
|
|
4074
|
+
const context = text.slice(start, end).replace(/\s+/g, " ").trim();
|
|
4075
|
+
results.push({
|
|
4076
|
+
pattern: sp,
|
|
4077
|
+
match: mask(matched),
|
|
4078
|
+
context
|
|
4079
|
+
});
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
return results;
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
// src/scanner/baas/bundle-analyzer.ts
|
|
4086
|
+
var MAX_BUNDLE_SIZE = 5 * 1024 * 1024;
|
|
4087
|
+
var MAX_BUNDLES = 10;
|
|
4088
|
+
var FETCH_TIMEOUT_MS = 15e3;
|
|
4089
|
+
function extractScriptSrcs(html, baseUrl) {
|
|
4090
|
+
const srcs = [];
|
|
4091
|
+
const scriptRe = /<script[^>]+src=["']([^"']+)["'][^>]*>/gi;
|
|
4092
|
+
for (const m of html.matchAll(scriptRe)) {
|
|
4093
|
+
const src = m[1];
|
|
4094
|
+
if (!src) continue;
|
|
4095
|
+
if (src.startsWith("data:")) continue;
|
|
4096
|
+
try {
|
|
4097
|
+
const resolved = new URL(src, baseUrl).toString();
|
|
4098
|
+
srcs.push(resolved);
|
|
4099
|
+
} catch {
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
return srcs;
|
|
4103
|
+
}
|
|
4104
|
+
async function fetchWithTimeout(url, timeoutMs) {
|
|
4105
|
+
const controller = new AbortController();
|
|
4106
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4107
|
+
try {
|
|
4108
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
4109
|
+
return response;
|
|
4110
|
+
} finally {
|
|
4111
|
+
clearTimeout(timer);
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
async function fetchText(url) {
|
|
4115
|
+
try {
|
|
4116
|
+
const res = await fetchWithTimeout(url, FETCH_TIMEOUT_MS);
|
|
4117
|
+
if (!res.ok) {
|
|
4118
|
+
return { text: "", error: `HTTP ${res.status} for ${url}` };
|
|
4119
|
+
}
|
|
4120
|
+
const contentLength = parseInt(res.headers.get("content-length") ?? "0", 10);
|
|
4121
|
+
if (contentLength > MAX_BUNDLE_SIZE) {
|
|
4122
|
+
return { text: "", error: `Bundle too large (${contentLength} bytes): ${url}` };
|
|
4123
|
+
}
|
|
4124
|
+
const buffer = await res.arrayBuffer();
|
|
4125
|
+
if (buffer.byteLength > MAX_BUNDLE_SIZE) {
|
|
4126
|
+
return { text: "", error: `Bundle exceeds 5 MB limit: ${url}` };
|
|
4127
|
+
}
|
|
4128
|
+
return { text: new TextDecoder().decode(buffer) };
|
|
4129
|
+
} catch (err) {
|
|
4130
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4131
|
+
return { text: "", error: `Fetch failed for ${url}: ${msg}` };
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
function makeSecretFinding(bundleUrl, matchedId, matchedName, severity, maskedValue, context, index) {
|
|
4135
|
+
return {
|
|
4136
|
+
id: `BU-${String(index + 1).padStart(3, "0")}`,
|
|
4137
|
+
category: "exposed-secret",
|
|
4138
|
+
severity,
|
|
4139
|
+
title: `Exposed ${matchedName} secret in JS bundle`,
|
|
4140
|
+
description: `A ${matchedName} credential was found in a compiled JavaScript bundle at ${bundleUrl}. Secrets baked into frontend bundles are readable by anyone who inspects your app's network traffic or source code.`,
|
|
4141
|
+
evidence: `Pattern: ${matchedId} | Masked value: ${maskedValue} | Context: ${context}`,
|
|
4142
|
+
recommendation: `Move this secret to a server-side environment variable. Never include API keys or tokens in frontend JavaScript. Use a backend proxy or BFF (Backend for Frontend) pattern to make authenticated API calls.`
|
|
4143
|
+
};
|
|
4144
|
+
}
|
|
4145
|
+
async function analyzeBundles(appUrl) {
|
|
4146
|
+
const errors = [];
|
|
4147
|
+
const findings = [];
|
|
4148
|
+
let bundlesAnalyzed = 0;
|
|
4149
|
+
const baseUrl = appUrl.endsWith("/") ? appUrl : `${appUrl}/`;
|
|
4150
|
+
const { text: html, error: htmlError } = await fetchText(baseUrl);
|
|
4151
|
+
if (htmlError || !html) {
|
|
4152
|
+
errors.push(htmlError ?? `Could not fetch root HTML for ${baseUrl}`);
|
|
4153
|
+
return { url: appUrl, bundlesAnalyzed: 0, findings, errors };
|
|
4154
|
+
}
|
|
4155
|
+
const scriptUrls = extractScriptSrcs(html, baseUrl).slice(0, MAX_BUNDLES);
|
|
4156
|
+
if (scriptUrls.length === 0) {
|
|
4157
|
+
errors.push(`No <script src> tags found in HTML at ${baseUrl}`);
|
|
4158
|
+
return { url: appUrl, bundlesAnalyzed: 0, findings, errors };
|
|
4159
|
+
}
|
|
4160
|
+
let findingIndex = 0;
|
|
4161
|
+
for (const scriptUrl of scriptUrls) {
|
|
4162
|
+
const { text: bundleText, error: fetchError } = await fetchText(scriptUrl);
|
|
4163
|
+
if (fetchError || !bundleText) {
|
|
4164
|
+
errors.push(fetchError ?? `Empty bundle at ${scriptUrl}`);
|
|
4165
|
+
continue;
|
|
4166
|
+
}
|
|
4167
|
+
bundlesAnalyzed++;
|
|
4168
|
+
const matches = matchSecrets(bundleText);
|
|
4169
|
+
for (const secretMatch of matches) {
|
|
4170
|
+
findings.push(
|
|
4171
|
+
makeSecretFinding(
|
|
4172
|
+
scriptUrl,
|
|
4173
|
+
secretMatch.pattern.id,
|
|
4174
|
+
secretMatch.pattern.name,
|
|
4175
|
+
secretMatch.pattern.severity,
|
|
4176
|
+
secretMatch.match,
|
|
4177
|
+
secretMatch.context,
|
|
4178
|
+
findingIndex++
|
|
4179
|
+
)
|
|
4180
|
+
);
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
return { url: appUrl, bundlesAnalyzed, findings, errors };
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
// src/scanner/baas/supabase-scanner.ts
|
|
4187
|
+
var FETCH_TIMEOUT_MS2 = 1e4;
|
|
4188
|
+
async function fetchWithTimeout2(url, init) {
|
|
4189
|
+
const controller = new AbortController();
|
|
4190
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
|
|
4191
|
+
try {
|
|
4192
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
4193
|
+
} finally {
|
|
4194
|
+
clearTimeout(timer);
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
function normaliseSupabaseUrl(url) {
|
|
4198
|
+
let u = url.replace(/\/+$/, "");
|
|
4199
|
+
if (!u.startsWith("http")) {
|
|
4200
|
+
u = `https://${u}`;
|
|
4201
|
+
}
|
|
4202
|
+
return u;
|
|
4203
|
+
}
|
|
4204
|
+
function extractAnonKeyFromFindings(findings) {
|
|
4205
|
+
for (const f of findings) {
|
|
4206
|
+
const ev = f.evidence ?? "";
|
|
4207
|
+
if ((ev.includes("supabase") || ev.includes("SP-022") || ev.includes("SP-023")) && ev.includes("eyJ")) {
|
|
4208
|
+
const jwtMatch = ev.match(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/);
|
|
4209
|
+
if (jwtMatch) return jwtMatch[0];
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
return null;
|
|
4213
|
+
}
|
|
4214
|
+
async function scanSupabase(opts) {
|
|
4215
|
+
const findings = [];
|
|
4216
|
+
const errors = [];
|
|
4217
|
+
const tablesFound = [];
|
|
4218
|
+
let anonReadExposed = false;
|
|
4219
|
+
let reachable = false;
|
|
4220
|
+
const baseUrl = normaliseSupabaseUrl(opts.projectUrl);
|
|
4221
|
+
try {
|
|
4222
|
+
const res = await fetchWithTimeout2(`${baseUrl}/rest/v1/`, {
|
|
4223
|
+
method: "HEAD"
|
|
4224
|
+
});
|
|
4225
|
+
reachable = res.status === 401 || res.status === 200 || res.status === 403;
|
|
4226
|
+
} catch {
|
|
4227
|
+
errors.push(`Supabase project not reachable at ${baseUrl}`);
|
|
4228
|
+
return { projectUrl: baseUrl, findings, tablesFound, anonReadExposed, reachable, errors };
|
|
4229
|
+
}
|
|
4230
|
+
if (!reachable) {
|
|
4231
|
+
errors.push(`Supabase project returned unexpected status at ${baseUrl}/rest/v1/`);
|
|
4232
|
+
return { projectUrl: baseUrl, findings, tablesFound, anonReadExposed, reachable, errors };
|
|
4233
|
+
}
|
|
4234
|
+
const bundleResult = await analyzeBundles(baseUrl);
|
|
4235
|
+
findings.push(...bundleResult.findings);
|
|
4236
|
+
if (bundleResult.errors.length > 0) {
|
|
4237
|
+
errors.push(...bundleResult.errors.map((e) => `[bundle] ${e}`));
|
|
4238
|
+
}
|
|
4239
|
+
const hasServiceRoleKey = bundleResult.findings.some(
|
|
4240
|
+
(f) => f.evidence?.includes("service_role") || f.evidence?.includes("SP-023")
|
|
4241
|
+
);
|
|
4242
|
+
if (hasServiceRoleKey) {
|
|
4243
|
+
findings.push({
|
|
4244
|
+
id: "SB-005",
|
|
4245
|
+
category: "exposed-secret",
|
|
4246
|
+
severity: "critical",
|
|
4247
|
+
title: "Supabase service_role key exposed in frontend bundle",
|
|
4248
|
+
description: "The Supabase service_role key was found in a JavaScript bundle. This key bypasses all Row Level Security policies and grants full read/write/delete access to every table and storage bucket. This is equivalent to database superuser access.",
|
|
4249
|
+
evidence: "Detected via bundle analysis \u2014 service_role JWT in compiled JS",
|
|
4250
|
+
recommendation: "Rotate the service_role key immediately in Supabase Dashboard > Settings > API. This key must NEVER appear in frontend code. Use it only in server-side functions (Edge Functions, API routes)."
|
|
4251
|
+
});
|
|
4252
|
+
}
|
|
4253
|
+
let anonKey = opts.anonKey ?? null;
|
|
4254
|
+
if (!anonKey) {
|
|
4255
|
+
anonKey = extractAnonKeyFromFindings(bundleResult.findings);
|
|
4256
|
+
}
|
|
4257
|
+
if (!anonKey) {
|
|
4258
|
+
errors.push(
|
|
4259
|
+
"No anon key provided or detected in bundles \u2014 skipping RLS and table enumeration. Pass --supabase-key or ensure the app URL serves JS bundles with the anon key."
|
|
4260
|
+
);
|
|
4261
|
+
}
|
|
4262
|
+
if (anonKey) {
|
|
4263
|
+
try {
|
|
4264
|
+
const tablesRes = await fetchWithTimeout2(`${baseUrl}/rest/v1/`, {
|
|
4265
|
+
headers: {
|
|
4266
|
+
apikey: anonKey,
|
|
4267
|
+
Authorization: `Bearer ${anonKey}`
|
|
4268
|
+
}
|
|
4269
|
+
});
|
|
4270
|
+
if (tablesRes.ok) {
|
|
4271
|
+
try {
|
|
4272
|
+
const schema = await tablesRes.json();
|
|
4273
|
+
const paths = schema.paths ?? {};
|
|
4274
|
+
for (const path of Object.keys(paths)) {
|
|
4275
|
+
const tableName = path.replace(/^\//, "").split("?")[0];
|
|
4276
|
+
if (tableName && !tableName.includes("/")) {
|
|
4277
|
+
tablesFound.push(tableName);
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
} catch {
|
|
4281
|
+
errors.push("Could not parse Supabase REST schema response");
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
} catch (err) {
|
|
4285
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4286
|
+
errors.push(`Table enumeration failed: ${msg}`);
|
|
4287
|
+
}
|
|
4288
|
+
for (const table of tablesFound) {
|
|
4289
|
+
try {
|
|
4290
|
+
const readRes = await fetchWithTimeout2(
|
|
4291
|
+
`${baseUrl}/rest/v1/${table}?select=*&limit=1`,
|
|
4292
|
+
{
|
|
4293
|
+
headers: {
|
|
4294
|
+
apikey: anonKey,
|
|
4295
|
+
Authorization: `Bearer ${anonKey}`
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
);
|
|
4299
|
+
if (readRes.ok) {
|
|
4300
|
+
const body = await readRes.text();
|
|
4301
|
+
if (body.startsWith("[") && body !== "[]") {
|
|
4302
|
+
anonReadExposed = true;
|
|
4303
|
+
findings.push({
|
|
4304
|
+
id: "SB-001",
|
|
4305
|
+
category: "rls-misconfiguration",
|
|
4306
|
+
severity: "critical",
|
|
4307
|
+
title: `RLS disabled: anonymous read on "${table}"`,
|
|
4308
|
+
description: `Table "${table}" returned data via the anon key without any Row Level Security policies. Any user with the anon key (which is public) can read all rows in this table. This is the #1 Supabase security mistake.`,
|
|
4309
|
+
evidence: `GET /rest/v1/${table}?select=*&limit=1 returned 200 with data`,
|
|
4310
|
+
recommendation: `Enable RLS on table "${table}" in Supabase Dashboard > Database > Tables. Add a policy like: CREATE POLICY "auth read" ON "${table}" FOR SELECT USING (auth.uid() = user_id);`
|
|
4311
|
+
});
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
} catch {
|
|
4315
|
+
}
|
|
4316
|
+
try {
|
|
4317
|
+
const writeRes = await fetchWithTimeout2(
|
|
4318
|
+
`${baseUrl}/rest/v1/${table}`,
|
|
4319
|
+
{
|
|
4320
|
+
method: "POST",
|
|
4321
|
+
headers: {
|
|
4322
|
+
apikey: anonKey,
|
|
4323
|
+
Authorization: `Bearer ${anonKey}`,
|
|
4324
|
+
"Content-Type": "application/json",
|
|
4325
|
+
Prefer: "return=minimal"
|
|
4326
|
+
},
|
|
4327
|
+
body: JSON.stringify({})
|
|
4328
|
+
}
|
|
4329
|
+
);
|
|
4330
|
+
if (writeRes.status === 400 || writeRes.status === 201 || writeRes.status === 200) {
|
|
4331
|
+
findings.push({
|
|
4332
|
+
id: "SB-006",
|
|
4333
|
+
category: "rls-misconfiguration",
|
|
4334
|
+
severity: "critical",
|
|
4335
|
+
title: `RLS disabled: anonymous write on "${table}"`,
|
|
4336
|
+
description: `Table "${table}" allows INSERT operations via the anon key. The request reached the database layer (RLS did not block it). Even if it failed on a constraint, the lack of RLS means anyone can attempt to write data to this table.`,
|
|
4337
|
+
evidence: `POST /rest/v1/${table} returned ${writeRes.status} (not 401/403)`,
|
|
4338
|
+
recommendation: `Enable RLS on table "${table}" and add INSERT policies. For example: CREATE POLICY "auth insert" ON "${table}" FOR INSERT WITH CHECK (auth.uid() = user_id);`
|
|
4339
|
+
});
|
|
4340
|
+
}
|
|
4341
|
+
} catch {
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
if (anonKey) {
|
|
4346
|
+
try {
|
|
4347
|
+
const authRes = await fetchWithTimeout2(`${baseUrl}/auth/v1/settings`, {
|
|
4348
|
+
headers: {
|
|
4349
|
+
apikey: anonKey,
|
|
4350
|
+
Authorization: `Bearer ${anonKey}`
|
|
4351
|
+
}
|
|
4352
|
+
});
|
|
4353
|
+
if (authRes.ok) {
|
|
4354
|
+
try {
|
|
4355
|
+
const settings = await authRes.json();
|
|
4356
|
+
const autoconfirm = settings.mailer_autoconfirm ?? settings.autoconfirm;
|
|
4357
|
+
if (autoconfirm === true) {
|
|
4358
|
+
findings.push({
|
|
4359
|
+
id: "SB-003",
|
|
4360
|
+
category: "auth-misconfiguration",
|
|
4361
|
+
severity: "medium",
|
|
4362
|
+
title: "Email confirmation disabled (autoconfirm enabled)",
|
|
4363
|
+
description: "Supabase auth is configured to automatically confirm email addresses without requiring the user to click a verification link. This allows attackers to create accounts with any email address, including impersonating legitimate users.",
|
|
4364
|
+
evidence: "GET /auth/v1/settings returned mailer_autoconfirm: true",
|
|
4365
|
+
recommendation: "Disable autoconfirm in Supabase Dashboard > Authentication > Settings > Email Auth. Require email verification for all new signups."
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4368
|
+
const disableSignup = settings.disable_signup;
|
|
4369
|
+
if (disableSignup === false) {
|
|
4370
|
+
findings.push({
|
|
4371
|
+
id: "SB-007",
|
|
4372
|
+
category: "auth-misconfiguration",
|
|
4373
|
+
severity: "low",
|
|
4374
|
+
title: "Open signup enabled",
|
|
4375
|
+
description: "Public signup is enabled on this Supabase project. If this is an internal tool or admin panel, open signup allows anyone to create an account.",
|
|
4376
|
+
evidence: "GET /auth/v1/settings returned disable_signup: false",
|
|
4377
|
+
recommendation: "If this project is not meant for public registration, disable signup in Supabase Dashboard > Authentication > Settings."
|
|
4378
|
+
});
|
|
4379
|
+
}
|
|
4380
|
+
} catch {
|
|
4381
|
+
errors.push("Could not parse auth settings response");
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
} catch {
|
|
4385
|
+
errors.push("Auth settings endpoint not reachable");
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
try {
|
|
4389
|
+
const corsRes = await fetchWithTimeout2(`${baseUrl}/rest/v1/`, {
|
|
4390
|
+
method: "OPTIONS",
|
|
4391
|
+
headers: {
|
|
4392
|
+
Origin: "https://evil-attacker-site.com",
|
|
4393
|
+
"Access-Control-Request-Method": "GET"
|
|
4394
|
+
}
|
|
4395
|
+
});
|
|
4396
|
+
const allowOrigin = corsRes.headers.get("access-control-allow-origin");
|
|
4397
|
+
if (allowOrigin === "*" || allowOrigin === "https://evil-attacker-site.com") {
|
|
4398
|
+
const isReflected = allowOrigin === "https://evil-attacker-site.com";
|
|
4399
|
+
findings.push({
|
|
4400
|
+
id: "SB-004",
|
|
4401
|
+
category: "cors-misconfiguration",
|
|
4402
|
+
severity: "medium",
|
|
4403
|
+
title: isReflected ? "CORS reflects arbitrary origins on REST API" : "CORS wildcard policy on REST API",
|
|
4404
|
+
description: isReflected ? "The Supabase REST API reflects any Origin header back in Access-Control-Allow-Origin, which is functionally equivalent to a wildcard policy but harder to detect." : "The Supabase REST API returns Access-Control-Allow-Origin: * which allows any website to make authenticated requests to your API. Combined with an exposed anon key, this enables cross-origin data access from malicious sites.",
|
|
4405
|
+
evidence: isReflected ? "OPTIONS /rest/v1/ reflected origin: https://evil-attacker-site.com" : "OPTIONS /rest/v1/ returned Access-Control-Allow-Origin: *",
|
|
4406
|
+
recommendation: "Configure allowed origins in Supabase Dashboard > Settings > API. Restrict to your app domain(s) only."
|
|
4407
|
+
});
|
|
4408
|
+
}
|
|
4409
|
+
} catch {
|
|
4410
|
+
}
|
|
4411
|
+
return {
|
|
4412
|
+
projectUrl: baseUrl,
|
|
4413
|
+
findings,
|
|
4414
|
+
tablesFound,
|
|
4415
|
+
anonReadExposed,
|
|
4416
|
+
reachable,
|
|
4417
|
+
errors
|
|
4418
|
+
};
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
// src/scanner/baas/firebase-scanner.ts
|
|
4422
|
+
var FETCH_TIMEOUT_MS3 = 1e4;
|
|
4423
|
+
async function fetchWithTimeout3(url, init) {
|
|
4424
|
+
const controller = new AbortController();
|
|
4425
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS3);
|
|
4426
|
+
try {
|
|
4427
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
4428
|
+
} finally {
|
|
4429
|
+
clearTimeout(timer);
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
function normaliseUrl(url) {
|
|
4433
|
+
let u = url.replace(/\/+$/, "");
|
|
4434
|
+
if (!u.startsWith("http")) {
|
|
4435
|
+
u = `https://${u}`;
|
|
4436
|
+
}
|
|
4437
|
+
return u;
|
|
4438
|
+
}
|
|
4439
|
+
function extractProjectId(url) {
|
|
4440
|
+
try {
|
|
4441
|
+
const hostname = new URL(url).hostname;
|
|
4442
|
+
const match = hostname.match(/^([^.]+)\.(web\.app|firebaseapp\.com)$/);
|
|
4443
|
+
return match?.[1] ?? null;
|
|
4444
|
+
} catch {
|
|
4445
|
+
return null;
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
function extractConfigFromFindings(findings) {
|
|
4449
|
+
const config = {};
|
|
4450
|
+
for (const f of findings) {
|
|
4451
|
+
const ev = f.evidence ?? "";
|
|
4452
|
+
if (ev.includes("AIzaSy")) {
|
|
4453
|
+
const keyMatch = ev.match(/AIzaSy[A-Za-z0-9_-]{33}/);
|
|
4454
|
+
if (keyMatch) config.apiKey = keyMatch[0];
|
|
4455
|
+
}
|
|
4456
|
+
if (ev.includes("projectId") || ev.includes("firebase")) {
|
|
4457
|
+
const projMatch = ev.match(/["']([a-z0-9-]+)\.firebaseapp\.com["']/);
|
|
4458
|
+
if (projMatch) config.projectId = projMatch[1];
|
|
4459
|
+
}
|
|
4460
|
+
if (ev.includes("storageBucket") || ev.includes(".appspot.com")) {
|
|
4461
|
+
const bucketMatch = ev.match(/([a-z0-9-]+)\.appspot\.com/);
|
|
4462
|
+
if (bucketMatch) config.storageBucket = bucketMatch[0];
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
return config;
|
|
4466
|
+
}
|
|
4467
|
+
async function scanFirebase(opts) {
|
|
4468
|
+
const findings = [];
|
|
4469
|
+
const errors = [];
|
|
4470
|
+
let firestorePublicAccess = false;
|
|
4471
|
+
let rtdbPublicAccess = false;
|
|
4472
|
+
let configExposed = false;
|
|
4473
|
+
let reachable = false;
|
|
4474
|
+
const baseUrl = normaliseUrl(opts.projectUrl);
|
|
4475
|
+
const projectId = opts.projectId ?? extractProjectId(baseUrl);
|
|
4476
|
+
try {
|
|
4477
|
+
const res = await fetchWithTimeout3(baseUrl, { method: "HEAD" });
|
|
4478
|
+
reachable = res.status >= 200 && res.status < 500;
|
|
4479
|
+
} catch {
|
|
4480
|
+
errors.push(`Firebase project not reachable at ${baseUrl}`);
|
|
4481
|
+
return {
|
|
4482
|
+
projectUrl: baseUrl,
|
|
4483
|
+
projectId,
|
|
4484
|
+
findings,
|
|
4485
|
+
firestorePublicAccess,
|
|
4486
|
+
rtdbPublicAccess,
|
|
4487
|
+
configExposed,
|
|
4488
|
+
reachable,
|
|
4489
|
+
errors
|
|
4490
|
+
};
|
|
4491
|
+
}
|
|
4492
|
+
if (!reachable) {
|
|
4493
|
+
errors.push(`Firebase project returned unexpected status at ${baseUrl}`);
|
|
4494
|
+
return {
|
|
4495
|
+
projectUrl: baseUrl,
|
|
4496
|
+
projectId,
|
|
4497
|
+
findings,
|
|
4498
|
+
firestorePublicAccess,
|
|
4499
|
+
rtdbPublicAccess,
|
|
4500
|
+
configExposed,
|
|
4501
|
+
reachable,
|
|
4502
|
+
errors
|
|
4503
|
+
};
|
|
4504
|
+
}
|
|
4505
|
+
const bundleResult = await analyzeBundles(baseUrl);
|
|
4506
|
+
findings.push(...bundleResult.findings);
|
|
4507
|
+
if (bundleResult.errors.length > 0) {
|
|
4508
|
+
errors.push(...bundleResult.errors.map((e) => `[bundle] ${e}`));
|
|
4509
|
+
}
|
|
4510
|
+
const bundleConfig = extractConfigFromFindings(bundleResult.findings);
|
|
4511
|
+
const detectedProjectId = projectId ?? bundleConfig.projectId ?? null;
|
|
4512
|
+
if (bundleConfig.apiKey) {
|
|
4513
|
+
configExposed = true;
|
|
4514
|
+
findings.push({
|
|
4515
|
+
id: "FB-003",
|
|
4516
|
+
category: "exposed-secret",
|
|
4517
|
+
severity: "high",
|
|
4518
|
+
title: "Firebase config object exposed in frontend bundle",
|
|
4519
|
+
description: "The Firebase client configuration (apiKey, projectId, etc.) was found in a JavaScript bundle. While Firebase API keys are designed to be public for client-side use, exposure without App Check enforcement allows abuse: automated account creation, Firestore/RTDB enumeration, and quota exhaustion attacks.",
|
|
4520
|
+
evidence: `Firebase apiKey: ${bundleConfig.apiKey.slice(0, 8)}*** detected in bundle`,
|
|
4521
|
+
recommendation: "Enable Firebase App Check in the Firebase Console to restrict API access to your legitimate app. Configure reCAPTCHA Enterprise or DeviceCheck attestation. Without App Check, anyone with the config can call your Firebase APIs."
|
|
4522
|
+
});
|
|
4523
|
+
}
|
|
4524
|
+
if (detectedProjectId) {
|
|
4525
|
+
const firestoreUrl = `https://firestore.googleapis.com/v1/projects/${detectedProjectId}/databases/(default)/documents`;
|
|
4526
|
+
try {
|
|
4527
|
+
const fsRes = await fetchWithTimeout3(firestoreUrl);
|
|
4528
|
+
if (fsRes.ok) {
|
|
4529
|
+
firestorePublicAccess = true;
|
|
4530
|
+
let docCount = 0;
|
|
4531
|
+
try {
|
|
4532
|
+
const body = await fsRes.json();
|
|
4533
|
+
docCount = body.documents?.length ?? 0;
|
|
4534
|
+
} catch {
|
|
4535
|
+
}
|
|
4536
|
+
findings.push({
|
|
4537
|
+
id: "FB-001",
|
|
4538
|
+
category: "firebase-rules-issue",
|
|
4539
|
+
severity: "critical",
|
|
4540
|
+
title: "Firestore allows unauthenticated read access",
|
|
4541
|
+
description: "The Firestore REST API returned documents without any authentication token. This means Firestore Security Rules are configured with `allow read: if true` or similar permissive rules at the database or collection level. Any data in Firestore is publicly accessible.",
|
|
4542
|
+
evidence: `GET ${firestoreUrl} returned 200` + (docCount > 0 ? ` (${docCount} documents visible)` : ""),
|
|
4543
|
+
recommendation: "Update Firestore Security Rules to require authentication: `allow read: if request.auth != null;` at minimum. Ideally, add per-user rules: `allow read: if request.auth.uid == resource.data.userId;`"
|
|
4544
|
+
});
|
|
4545
|
+
}
|
|
4546
|
+
} catch (err) {
|
|
4547
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4548
|
+
errors.push(`Firestore probe failed: ${msg}`);
|
|
4549
|
+
}
|
|
4550
|
+
try {
|
|
4551
|
+
const writeUrl = `https://firestore.googleapis.com/v1/projects/${detectedProjectId}/databases/(default)/documents/vigile_probe_test`;
|
|
4552
|
+
const writeRes = await fetchWithTimeout3(writeUrl, {
|
|
4553
|
+
method: "POST",
|
|
4554
|
+
headers: { "Content-Type": "application/json" },
|
|
4555
|
+
body: JSON.stringify({
|
|
4556
|
+
fields: {
|
|
4557
|
+
_vigile_probe: { stringValue: "security_test" }
|
|
4558
|
+
}
|
|
4559
|
+
})
|
|
4560
|
+
});
|
|
4561
|
+
if (writeRes.status === 200) {
|
|
4562
|
+
firestorePublicAccess = true;
|
|
4563
|
+
findings.push({
|
|
4564
|
+
id: "FB-001",
|
|
4565
|
+
category: "firebase-rules-issue",
|
|
4566
|
+
severity: "critical",
|
|
4567
|
+
title: "Firestore allows unauthenticated WRITE access",
|
|
4568
|
+
description: "A document was successfully written to Firestore without any authentication. This means anyone on the internet can create, modify, or delete data in your database.",
|
|
4569
|
+
evidence: `POST to Firestore documents endpoint returned 200`,
|
|
4570
|
+
recommendation: "Immediately update Firestore Security Rules to block unauthenticated writes: `allow write: if request.auth != null;` \u2014 and review all existing data for tampering."
|
|
4571
|
+
});
|
|
4572
|
+
}
|
|
4573
|
+
} catch {
|
|
4574
|
+
}
|
|
4575
|
+
} else {
|
|
4576
|
+
errors.push(
|
|
4577
|
+
"Could not determine Firebase project ID \u2014 skipping Firestore/RTDB probes. Provide --firebase-project-id or use a *.web.app / *.firebaseapp.com URL."
|
|
4578
|
+
);
|
|
4579
|
+
}
|
|
4580
|
+
if (detectedProjectId) {
|
|
4581
|
+
const rtdbUrls = [
|
|
4582
|
+
`https://${detectedProjectId}-default-rtdb.firebaseio.com/.json?shallow=true`,
|
|
4583
|
+
`https://${detectedProjectId}.firebaseio.com/.json?shallow=true`
|
|
4584
|
+
];
|
|
4585
|
+
for (const rtdbUrl of rtdbUrls) {
|
|
4586
|
+
try {
|
|
4587
|
+
const rtdbRes = await fetchWithTimeout3(rtdbUrl);
|
|
4588
|
+
if (rtdbRes.ok) {
|
|
4589
|
+
rtdbPublicAccess = true;
|
|
4590
|
+
let keyCount = 0;
|
|
4591
|
+
try {
|
|
4592
|
+
const body = await rtdbRes.json();
|
|
4593
|
+
if (body && typeof body === "object") {
|
|
4594
|
+
keyCount = Object.keys(body).length;
|
|
4595
|
+
}
|
|
4596
|
+
} catch {
|
|
4597
|
+
}
|
|
4598
|
+
findings.push({
|
|
4599
|
+
id: "FB-002",
|
|
4600
|
+
category: "firebase-rules-issue",
|
|
4601
|
+
severity: "critical",
|
|
4602
|
+
title: "Realtime Database allows unauthenticated read access",
|
|
4603
|
+
description: 'The Firebase Realtime Database returned data at the root path without any authentication. RTDB rules are configured with `".read": true` or `".read": "auth == null"` or similar. All data stored in RTDB is publicly accessible.',
|
|
4604
|
+
evidence: `GET ${rtdbUrl.replace(/\?.*/, "")} returned 200` + (keyCount > 0 ? ` (${keyCount} top-level keys)` : ""),
|
|
4605
|
+
recommendation: 'Update RTDB Security Rules to require authentication: `".read": "auth != null"` at minimum. Review what data is stored in RTDB and assume it has been scraped if this rule was open.'
|
|
4606
|
+
});
|
|
4607
|
+
break;
|
|
4608
|
+
}
|
|
4609
|
+
} catch {
|
|
4610
|
+
continue;
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
try {
|
|
4614
|
+
const rtdbWriteUrl = `https://${detectedProjectId}-default-rtdb.firebaseio.com/vigile_probe_test.json`;
|
|
4615
|
+
const rtdbWriteRes = await fetchWithTimeout3(rtdbWriteUrl, {
|
|
4616
|
+
method: "PUT",
|
|
4617
|
+
headers: { "Content-Type": "application/json" },
|
|
4618
|
+
body: JSON.stringify({ _vigile_probe: "security_test" })
|
|
4619
|
+
});
|
|
4620
|
+
if (rtdbWriteRes.ok) {
|
|
4621
|
+
rtdbPublicAccess = true;
|
|
4622
|
+
findings.push({
|
|
4623
|
+
id: "FB-002",
|
|
4624
|
+
category: "firebase-rules-issue",
|
|
4625
|
+
severity: "critical",
|
|
4626
|
+
title: "Realtime Database allows unauthenticated WRITE access",
|
|
4627
|
+
description: "Data was successfully written to Firebase Realtime Database without any authentication. Anyone can create, modify, or delete data.",
|
|
4628
|
+
evidence: `PUT to RTDB vigile_probe_test.json returned 200`,
|
|
4629
|
+
recommendation: 'Immediately update RTDB Security Rules: `".write": "auth != null"`. Audit all existing data for tampering. Consider migrating sensitive data to Firestore with stricter per-document rules.'
|
|
4630
|
+
});
|
|
4631
|
+
}
|
|
4632
|
+
} catch {
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4635
|
+
if (detectedProjectId) {
|
|
4636
|
+
const storageBucket = bundleConfig.storageBucket ?? `${detectedProjectId}.appspot.com`;
|
|
4637
|
+
const storageUrl = `https://firebasestorage.googleapis.com/v0/b/${storageBucket}/o`;
|
|
4638
|
+
try {
|
|
4639
|
+
const storageRes = await fetchWithTimeout3(storageUrl);
|
|
4640
|
+
if (storageRes.ok) {
|
|
4641
|
+
let itemCount = 0;
|
|
4642
|
+
try {
|
|
4643
|
+
const body = await storageRes.json();
|
|
4644
|
+
itemCount = body.items?.length ?? 0;
|
|
4645
|
+
} catch {
|
|
4646
|
+
}
|
|
4647
|
+
findings.push({
|
|
4648
|
+
id: "FB-004",
|
|
4649
|
+
category: "firebase-rules-issue",
|
|
4650
|
+
severity: "high",
|
|
4651
|
+
title: "Firebase Storage bucket is publicly listable",
|
|
4652
|
+
description: "The Firebase Storage bucket returned a file listing without authentication. Storage Security Rules allow public read access. All files in the bucket can be enumerated and downloaded by anyone.",
|
|
4653
|
+
evidence: `GET ${storageUrl} returned 200` + (itemCount > 0 ? ` (${itemCount} files visible)` : ""),
|
|
4654
|
+
recommendation: "Update Firebase Storage Security Rules to require authentication: `allow read: if request.auth != null;` \u2014 Review uploaded files for sensitive content (user uploads, profile pictures, documents)."
|
|
4655
|
+
});
|
|
4656
|
+
}
|
|
4657
|
+
} catch {
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4660
|
+
try {
|
|
4661
|
+
const headRes = await fetchWithTimeout3(baseUrl);
|
|
4662
|
+
const headers = headRes.headers;
|
|
4663
|
+
const missingHeaders = [];
|
|
4664
|
+
if (!headers.get("x-content-type-options")) {
|
|
4665
|
+
missingHeaders.push("X-Content-Type-Options");
|
|
4666
|
+
}
|
|
4667
|
+
if (!headers.get("x-frame-options") && !headers.get("content-security-policy")?.includes("frame-ancestors")) {
|
|
4668
|
+
missingHeaders.push("X-Frame-Options or CSP frame-ancestors");
|
|
4669
|
+
}
|
|
4670
|
+
if (!headers.get("strict-transport-security")) {
|
|
4671
|
+
missingHeaders.push("Strict-Transport-Security");
|
|
4672
|
+
}
|
|
4673
|
+
if (missingHeaders.length >= 2) {
|
|
4674
|
+
findings.push({
|
|
4675
|
+
id: "FB-005",
|
|
4676
|
+
category: "cors-misconfiguration",
|
|
4677
|
+
severity: "medium",
|
|
4678
|
+
title: "Firebase Hosting missing security headers",
|
|
4679
|
+
description: `The Firebase Hosting response is missing ${missingHeaders.length} recommended security headers: ${missingHeaders.join(", ")}. These headers protect against clickjacking, MIME sniffing, and protocol downgrade attacks.`,
|
|
4680
|
+
evidence: `Missing: ${missingHeaders.join(", ")}`,
|
|
4681
|
+
recommendation: 'Add security headers in firebase.json under hosting.headers: {"source": "**", "headers": [{"key": "X-Content-Type-Options", "value": "nosniff"}, {"key": "X-Frame-Options", "value": "DENY"}]}'
|
|
4682
|
+
});
|
|
4683
|
+
}
|
|
4684
|
+
} catch {
|
|
4685
|
+
}
|
|
4686
|
+
return {
|
|
4687
|
+
projectUrl: baseUrl,
|
|
4688
|
+
projectId: detectedProjectId,
|
|
4689
|
+
findings,
|
|
4690
|
+
firestorePublicAccess,
|
|
4691
|
+
rtdbPublicAccess,
|
|
4692
|
+
configExposed,
|
|
4693
|
+
reachable,
|
|
4694
|
+
errors
|
|
4695
|
+
};
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4698
|
+
// src/scanner/baas/cve-detector.ts
|
|
4699
|
+
var FETCH_TIMEOUT_MS4 = 15e3;
|
|
4700
|
+
var OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch";
|
|
4701
|
+
var MAX_BATCH_SIZE = 100;
|
|
4702
|
+
async function fetchWithTimeout4(url, init) {
|
|
4703
|
+
const controller = new AbortController();
|
|
4704
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS4);
|
|
4705
|
+
try {
|
|
4706
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
4707
|
+
} finally {
|
|
4708
|
+
clearTimeout(timer);
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
function extractCvssScore(vuln) {
|
|
4712
|
+
if (vuln.severity && vuln.severity.length > 0) {
|
|
4713
|
+
for (const sev of vuln.severity) {
|
|
4714
|
+
const scoreMatch = sev.score.match(/CVSS:[^/]+\/.*?/);
|
|
4715
|
+
if (scoreMatch && sev.type === "CVSS_V3") {
|
|
4716
|
+
return estimateCvssFromVector(sev.score);
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
return 5;
|
|
4721
|
+
}
|
|
4722
|
+
function estimateCvssFromVector(vector) {
|
|
4723
|
+
const v = vector.toUpperCase();
|
|
4724
|
+
const avNetwork = v.includes("/AV:N");
|
|
4725
|
+
const prNone = v.includes("/PR:N");
|
|
4726
|
+
const cHigh = v.includes("/C:H");
|
|
4727
|
+
const iHigh = v.includes("/I:H");
|
|
4728
|
+
const aHigh = v.includes("/A:H");
|
|
4729
|
+
const cLow = v.includes("/C:L");
|
|
4730
|
+
const iLow = v.includes("/I:L");
|
|
4731
|
+
const aLow = v.includes("/A:L");
|
|
4732
|
+
let score = 5;
|
|
4733
|
+
if (avNetwork) score += 1.5;
|
|
4734
|
+
if (prNone) score += 1;
|
|
4735
|
+
if (cHigh) score += 0.8;
|
|
4736
|
+
if (iHigh) score += 0.8;
|
|
4737
|
+
if (aHigh) score += 0.8;
|
|
4738
|
+
if (cLow) score += 0.3;
|
|
4739
|
+
if (iLow) score += 0.3;
|
|
4740
|
+
if (aLow) score += 0.3;
|
|
4741
|
+
return Math.min(10, Math.round(score * 10) / 10);
|
|
4742
|
+
}
|
|
4743
|
+
function extractPatchedVersion(vuln, pkgName) {
|
|
4744
|
+
if (!vuln.affected) return null;
|
|
4745
|
+
for (const affected of vuln.affected) {
|
|
4746
|
+
if (affected.package.name !== pkgName) continue;
|
|
4747
|
+
if (!affected.ranges) continue;
|
|
4748
|
+
for (const range of affected.ranges) {
|
|
4749
|
+
for (const event of range.events) {
|
|
4750
|
+
if (event.fixed) return event.fixed;
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
return null;
|
|
4755
|
+
}
|
|
4756
|
+
function getPreferredId(vuln) {
|
|
4757
|
+
if (vuln.aliases) {
|
|
4758
|
+
const cve = vuln.aliases.find((a) => a.startsWith("CVE-"));
|
|
4759
|
+
if (cve) return cve;
|
|
4760
|
+
}
|
|
4761
|
+
return vuln.id;
|
|
4762
|
+
}
|
|
4763
|
+
function cvssToSeverity(score) {
|
|
4764
|
+
if (score >= 9) return "critical";
|
|
4765
|
+
if (score >= 7) return "high";
|
|
4766
|
+
if (score >= 4) return "medium";
|
|
4767
|
+
return "low";
|
|
4768
|
+
}
|
|
4769
|
+
async function detectCves(packages) {
|
|
4770
|
+
const matches = [];
|
|
4771
|
+
const findings = [];
|
|
4772
|
+
const errors = [];
|
|
4773
|
+
if (packages.length === 0) {
|
|
4774
|
+
return { packagesChecked: 0, matches, findings, errors };
|
|
4775
|
+
}
|
|
4776
|
+
const queryablePackages = packages.filter(
|
|
4777
|
+
(p) => p.ecosystem !== "unknown" && p.version && !p.version.includes("*")
|
|
4778
|
+
);
|
|
4779
|
+
if (queryablePackages.length === 0) {
|
|
4780
|
+
errors.push("No packages with valid version + ecosystem for CVE lookup");
|
|
4781
|
+
return { packagesChecked: packages.length, matches, findings, errors };
|
|
4782
|
+
}
|
|
4783
|
+
const chunks = [];
|
|
4784
|
+
for (let i = 0; i < queryablePackages.length; i += MAX_BATCH_SIZE) {
|
|
4785
|
+
chunks.push(queryablePackages.slice(i, i + MAX_BATCH_SIZE));
|
|
4786
|
+
}
|
|
4787
|
+
for (const chunk of chunks) {
|
|
4788
|
+
const queries = chunk.map((pkg) => ({
|
|
4789
|
+
package: {
|
|
4790
|
+
name: pkg.name,
|
|
4791
|
+
ecosystem: pkg.ecosystem === "npm" ? "npm" : pkg.ecosystem === "pypi" ? "PyPI" : pkg.ecosystem
|
|
4792
|
+
},
|
|
4793
|
+
version: pkg.version
|
|
4794
|
+
}));
|
|
4795
|
+
try {
|
|
4796
|
+
const res = await fetchWithTimeout4(OSV_BATCH_URL, {
|
|
4797
|
+
method: "POST",
|
|
4798
|
+
headers: { "Content-Type": "application/json" },
|
|
4799
|
+
body: JSON.stringify({ queries })
|
|
4800
|
+
});
|
|
4801
|
+
if (!res.ok) {
|
|
4802
|
+
errors.push(`OSV.dev API returned HTTP ${res.status}`);
|
|
4803
|
+
continue;
|
|
4804
|
+
}
|
|
4805
|
+
const body = await res.json();
|
|
4806
|
+
for (let i = 0; i < body.results.length; i++) {
|
|
4807
|
+
const result = body.results[i];
|
|
4808
|
+
const pkg = chunk[i];
|
|
4809
|
+
if (!result?.vulns || result.vulns.length === 0) continue;
|
|
4810
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
4811
|
+
for (const vuln of result.vulns) {
|
|
4812
|
+
const vulnId = getPreferredId(vuln);
|
|
4813
|
+
if (seenIds.has(vulnId)) continue;
|
|
4814
|
+
seenIds.add(vulnId);
|
|
4815
|
+
const cvssScore = extractCvssScore(vuln);
|
|
4816
|
+
const patchedVersion = extractPatchedVersion(vuln, pkg.name);
|
|
4817
|
+
const summary = vuln.summary ?? vuln.details?.slice(0, 200) ?? "No description available";
|
|
4818
|
+
const match = {
|
|
4819
|
+
pkg,
|
|
4820
|
+
cveId: vulnId,
|
|
4821
|
+
cvssScore,
|
|
4822
|
+
summary,
|
|
4823
|
+
patchedVersion
|
|
4824
|
+
};
|
|
4825
|
+
matches.push(match);
|
|
4826
|
+
const severity = cvssToSeverity(cvssScore);
|
|
4827
|
+
findings.push({
|
|
4828
|
+
id: vulnId,
|
|
4829
|
+
category: "cve-detected",
|
|
4830
|
+
severity,
|
|
4831
|
+
title: `${vulnId}: ${pkg.name}@${pkg.version}`,
|
|
4832
|
+
description: `Known vulnerability in ${pkg.name} version ${pkg.version}. ${summary.slice(0, 300)}`,
|
|
4833
|
+
evidence: `Package: ${pkg.name}@${pkg.version} (${pkg.ecosystem}) | CVSS: ${cvssScore} | ` + (patchedVersion ? `Fixed in: ${patchedVersion}` : "No patch available"),
|
|
4834
|
+
recommendation: patchedVersion ? `Upgrade ${pkg.name} to version ${patchedVersion} or later. Run: npm install ${pkg.name}@${patchedVersion}` : `No patched version available. Consider removing or replacing ${pkg.name} with an alternative package. Monitor ${vulnId} for updates.`
|
|
4835
|
+
});
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
} catch (err) {
|
|
4839
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4840
|
+
errors.push(`OSV.dev query failed: ${msg}`);
|
|
4841
|
+
}
|
|
4842
|
+
}
|
|
4843
|
+
return {
|
|
4844
|
+
packagesChecked: packages.length,
|
|
4845
|
+
matches,
|
|
4846
|
+
findings,
|
|
4847
|
+
errors
|
|
4848
|
+
};
|
|
4849
|
+
}
|
|
4850
|
+
function parseNpmPackages(packageJsonText) {
|
|
4851
|
+
try {
|
|
4852
|
+
const parsed = JSON.parse(packageJsonText);
|
|
4853
|
+
const deps = {
|
|
4854
|
+
...parsed.dependencies ?? {},
|
|
4855
|
+
...parsed.devDependencies ?? {}
|
|
4856
|
+
};
|
|
4857
|
+
return Object.entries(deps).map(([name, version]) => ({
|
|
4858
|
+
name,
|
|
4859
|
+
version: String(version).replace(/^[\^~>=<]+/, ""),
|
|
4860
|
+
// strip semver range prefixes
|
|
4861
|
+
ecosystem: "npm"
|
|
4862
|
+
}));
|
|
4863
|
+
} catch {
|
|
4864
|
+
return [];
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
|
|
4868
|
+
// src/scanner/baas/vibe-app-scanner.ts
|
|
4869
|
+
var FETCH_TIMEOUT_MS5 = 1e4;
|
|
4870
|
+
async function fetchWithTimeout5(url, init) {
|
|
4871
|
+
const controller = new AbortController();
|
|
4872
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS5);
|
|
4873
|
+
try {
|
|
4874
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
4875
|
+
} finally {
|
|
4876
|
+
clearTimeout(timer);
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
function detectPlatform(findings, appUrl) {
|
|
4880
|
+
for (const f of findings) {
|
|
4881
|
+
const ev = (f.evidence ?? "").toLowerCase();
|
|
4882
|
+
const title = f.title.toLowerCase();
|
|
4883
|
+
if (ev.includes("supabase") || title.includes("supabase") || ev.includes("sb-") || ev.includes(".supabase.co")) {
|
|
4884
|
+
return "supabase";
|
|
4885
|
+
}
|
|
4886
|
+
if (ev.includes("firebase") || title.includes("firebase") || ev.includes("aizasy") || // Firebase API key prefix (lowercased)
|
|
4887
|
+
ev.includes(".firebaseapp.com") || ev.includes(".firebaseio.com")) {
|
|
4888
|
+
return "firebase";
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
try {
|
|
4892
|
+
const hostname = new URL(appUrl).hostname;
|
|
4893
|
+
if (hostname.endsWith(".supabase.co")) return "supabase";
|
|
4894
|
+
if (hostname.endsWith(".web.app") || hostname.endsWith(".firebaseapp.com")) {
|
|
4895
|
+
return "firebase";
|
|
4896
|
+
}
|
|
4897
|
+
} catch {
|
|
4898
|
+
}
|
|
4899
|
+
return "unknown";
|
|
4900
|
+
}
|
|
4901
|
+
async function tryFetchPackageJson(appUrl) {
|
|
4902
|
+
const baseUrl = appUrl.endsWith("/") ? appUrl : `${appUrl}/`;
|
|
4903
|
+
const paths = [
|
|
4904
|
+
"package.json",
|
|
4905
|
+
"assets/package.json"
|
|
4906
|
+
];
|
|
4907
|
+
for (const path of paths) {
|
|
4908
|
+
try {
|
|
4909
|
+
const res = await fetchWithTimeout5(`${baseUrl}${path}`);
|
|
4910
|
+
if (res.ok) {
|
|
4911
|
+
const text = await res.text();
|
|
4912
|
+
if (text.trimStart().startsWith("{") && text.includes('"dependencies"')) {
|
|
4913
|
+
return parseNpmPackages(text);
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
} catch {
|
|
4917
|
+
continue;
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
return [];
|
|
4921
|
+
}
|
|
4922
|
+
function extractPackagesFromBundleFindings(findings) {
|
|
4923
|
+
const packageNames = /* @__PURE__ */ new Set();
|
|
4924
|
+
for (const f of findings) {
|
|
4925
|
+
const ev = f.evidence ?? "";
|
|
4926
|
+
const nodeModuleMatches = ev.matchAll(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/g);
|
|
4927
|
+
for (const m of nodeModuleMatches) {
|
|
4928
|
+
if (m[1]) packageNames.add(m[1]);
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
return Array.from(packageNames);
|
|
4932
|
+
}
|
|
4933
|
+
function deduplicateFindings3(findings) {
|
|
4934
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4935
|
+
const unique = [];
|
|
4936
|
+
for (const f of findings) {
|
|
4937
|
+
const key = `${f.id}::${f.evidence ?? ""}`;
|
|
4938
|
+
if (seen.has(key)) continue;
|
|
4939
|
+
seen.add(key);
|
|
4940
|
+
unique.push(f);
|
|
4941
|
+
}
|
|
4942
|
+
return unique;
|
|
4943
|
+
}
|
|
4944
|
+
async function scanVibeApp(opts) {
|
|
4945
|
+
const allFindings = [];
|
|
4946
|
+
const errors = [];
|
|
4947
|
+
let packagesChecked = 0;
|
|
4948
|
+
let cveMatches = 0;
|
|
4949
|
+
const bundleResult = await analyzeBundles(opts.appUrl);
|
|
4950
|
+
allFindings.push(...bundleResult.findings);
|
|
4951
|
+
errors.push(...bundleResult.errors);
|
|
4952
|
+
const detectedPlatform = opts.platform ?? detectPlatform(bundleResult.findings, opts.appUrl);
|
|
4953
|
+
if (detectedPlatform === "supabase" || opts.supabaseUrl) {
|
|
4954
|
+
const supabaseUrl = opts.supabaseUrl ?? opts.appUrl;
|
|
4955
|
+
try {
|
|
4956
|
+
const supabaseResult = await scanSupabase({ projectUrl: supabaseUrl });
|
|
4957
|
+
allFindings.push(...supabaseResult.findings);
|
|
4958
|
+
errors.push(...supabaseResult.errors);
|
|
4959
|
+
} catch (err) {
|
|
4960
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4961
|
+
errors.push(`Supabase scan failed: ${msg}`);
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
if (detectedPlatform === "firebase" || opts.firebaseUrl) {
|
|
4965
|
+
const firebaseUrl = opts.firebaseUrl ?? opts.appUrl;
|
|
4966
|
+
try {
|
|
4967
|
+
const firebaseResult = await scanFirebase({ projectUrl: firebaseUrl });
|
|
4968
|
+
allFindings.push(...firebaseResult.findings);
|
|
4969
|
+
errors.push(...firebaseResult.errors);
|
|
4970
|
+
} catch (err) {
|
|
4971
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4972
|
+
errors.push(`Firebase scan failed: ${msg}`);
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
if (detectedPlatform === "unknown" && !opts.supabaseUrl && !opts.firebaseUrl) {
|
|
4976
|
+
errors.push(
|
|
4977
|
+
"Could not detect BaaS platform (Supabase or Firebase) from bundle analysis or URL. Provide --supabase <url> or --firebase <url> explicitly for deeper platform scanning."
|
|
4978
|
+
);
|
|
4979
|
+
}
|
|
4980
|
+
let packages = [];
|
|
4981
|
+
try {
|
|
4982
|
+
packages = await tryFetchPackageJson(opts.appUrl);
|
|
4983
|
+
} catch {
|
|
4984
|
+
}
|
|
4985
|
+
if (packages.length === 0) {
|
|
4986
|
+
const bundlePackageNames = extractPackagesFromBundleFindings(bundleResult.findings);
|
|
4987
|
+
if (bundlePackageNames.length > 0) {
|
|
4988
|
+
errors.push(
|
|
4989
|
+
`Detected ${bundlePackageNames.length} package(s) in bundles but could not determine versions for CVE lookup: ${bundlePackageNames.slice(0, 5).join(", ")}` + (bundlePackageNames.length > 5 ? "..." : "")
|
|
4990
|
+
);
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
if (packages.length > 0) {
|
|
4994
|
+
try {
|
|
4995
|
+
const cveResult = await detectCves(packages);
|
|
4996
|
+
allFindings.push(...cveResult.findings);
|
|
4997
|
+
errors.push(...cveResult.errors);
|
|
4998
|
+
packagesChecked = cveResult.packagesChecked;
|
|
4999
|
+
cveMatches = cveResult.matches.length;
|
|
5000
|
+
} catch (err) {
|
|
5001
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5002
|
+
errors.push(`CVE detection failed: ${msg}`);
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
const findings = deduplicateFindings3(allFindings);
|
|
5006
|
+
return {
|
|
5007
|
+
appUrl: opts.appUrl,
|
|
5008
|
+
detectedPlatform,
|
|
5009
|
+
findings,
|
|
5010
|
+
bundlesAnalyzed: bundleResult.bundlesAnalyzed,
|
|
5011
|
+
packagesChecked,
|
|
5012
|
+
cveMatches,
|
|
5013
|
+
errors
|
|
5014
|
+
};
|
|
5015
|
+
}
|
|
5016
|
+
|
|
2688
5017
|
// src/index.ts
|
|
2689
5018
|
var VERSION = require_package().version;
|
|
2690
5019
|
var program = new import_commander.Command();
|
|
@@ -2695,7 +5024,7 @@ function addScanOptions(cmd) {
|
|
|
2695
5024
|
return cmd.option("-j, --json", "Output results as JSON").option("-v, --verbose", "Show detailed findings and score breakdown").option("-c, --config <path>", "Path to a custom MCP config file").option("-o, --output <path>", "Write results to a file").option(
|
|
2696
5025
|
"--client <client>",
|
|
2697
5026
|
"Only scan a specific client (claude-desktop, cursor, claude-code, windsurf, vscode, openclaw)"
|
|
2698
|
-
).option("-s, --skills", "Scan agent skills only (SKILL.md, .mdc rules, CLAUDE.md, etc.)").option("-a, --all", "Scan both MCP servers and agent skills").option("--sentinel", "Enable Sentinel runtime monitoring (Pro+ feature)").option("--sentinel-server <name>", "Monitor a specific MCP server by name").option("--sentinel-duration <seconds>", "Monitoring duration in seconds (default: 120)", parseInt).option("--no-upload", "Skip uploading scan results to Vigile API");
|
|
5027
|
+
).option("-s, --skills", "Scan agent skills only (SKILL.md, .mdc rules, CLAUDE.md, etc.)").option("-a, --all", "Scan both MCP servers and agent skills").option("--sentinel", "Enable Sentinel runtime monitoring (Pro+ feature)").option("--sentinel-server <name>", "Monitor a specific MCP server by name").option("--sentinel-duration <seconds>", "Monitoring duration in seconds (default: 120)", parseInt).option("--no-upload", "Skip uploading scan results to Vigile API").option("--supabase <url>", "Scan a Supabase project URL for RLS issues and exposed keys").option("--supabase-key <key>", "Supabase anon key (auto-detected from bundles if omitted)").option("--firebase <url>", "Scan a Firebase project URL for rules issues and exposed keys").option("--app <url>", "Scan a deployed web app for exposed secrets and BaaS misconfigs");
|
|
2699
5028
|
}
|
|
2700
5029
|
addScanOptions(
|
|
2701
5030
|
program.command("scan").description("Scan MCP server configurations and agent skill files on this machine")
|
|
@@ -2866,6 +5195,9 @@ async function runScan(options) {
|
|
|
2866
5195
|
if (options.sentinel) {
|
|
2867
5196
|
await runSentinel(options, results, isJSON);
|
|
2868
5197
|
}
|
|
5198
|
+
if (options.supabase || options.firebase || options.app) {
|
|
5199
|
+
await runBaaSScan(options, isJSON);
|
|
5200
|
+
}
|
|
2869
5201
|
if (summary.bySeverity.critical > 0 || summary.bySeverity.high > 0) {
|
|
2870
5202
|
await new Promise((resolve) => {
|
|
2871
5203
|
if (process.stdout.writableNeedDrain) {
|
|
@@ -2926,6 +5258,90 @@ async function uploadResults(mcpResults, skillResults, isJSON) {
|
|
|
2926
5258
|
printUploadSuccess(summary);
|
|
2927
5259
|
}
|
|
2928
5260
|
}
|
|
5261
|
+
async function runBaaSScan(options, isJSON) {
|
|
5262
|
+
const spinner = isJSON ? null : (0, import_ora.default)("Running BaaS security scan...").start();
|
|
5263
|
+
let totalFindings = 0;
|
|
5264
|
+
let criticalOrHigh = 0;
|
|
5265
|
+
if (options.supabase) {
|
|
5266
|
+
const result = await scanSupabase({
|
|
5267
|
+
projectUrl: options.supabase,
|
|
5268
|
+
anonKey: options.supabaseKey
|
|
5269
|
+
});
|
|
5270
|
+
totalFindings += result.findings.length;
|
|
5271
|
+
criticalOrHigh += result.findings.filter(
|
|
5272
|
+
(f) => f.severity === "critical" || f.severity === "high"
|
|
5273
|
+
).length;
|
|
5274
|
+
if (!isJSON) {
|
|
5275
|
+
spinner?.stop();
|
|
5276
|
+
console.log(import_chalk2.default.bold(`
|
|
5277
|
+
Supabase: ${options.supabase}`));
|
|
5278
|
+
if (result.errors.length > 0) {
|
|
5279
|
+
result.errors.forEach((e) => console.log(import_chalk2.default.yellow(` \u26A0 ${e}`)));
|
|
5280
|
+
}
|
|
5281
|
+
result.findings.forEach((f) => {
|
|
5282
|
+
const color = f.severity === "critical" ? import_chalk2.default.red : f.severity === "high" ? import_chalk2.default.yellow : import_chalk2.default.gray;
|
|
5283
|
+
console.log(color(` [${f.severity.toUpperCase()}] ${f.title}`));
|
|
5284
|
+
if (options.verbose) console.log(import_chalk2.default.gray(` ${f.description}`));
|
|
5285
|
+
});
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5288
|
+
if (options.firebase) {
|
|
5289
|
+
const result = await scanFirebase({ projectUrl: options.firebase });
|
|
5290
|
+
totalFindings += result.findings.length;
|
|
5291
|
+
criticalOrHigh += result.findings.filter(
|
|
5292
|
+
(f) => f.severity === "critical" || f.severity === "high"
|
|
5293
|
+
).length;
|
|
5294
|
+
if (!isJSON) {
|
|
5295
|
+
spinner?.stop();
|
|
5296
|
+
console.log(import_chalk2.default.bold(`
|
|
5297
|
+
Firebase: ${options.firebase}`));
|
|
5298
|
+
if (result.errors.length > 0) {
|
|
5299
|
+
result.errors.forEach((e) => console.log(import_chalk2.default.yellow(` \u26A0 ${e}`)));
|
|
5300
|
+
}
|
|
5301
|
+
result.findings.forEach((f) => {
|
|
5302
|
+
const color = f.severity === "critical" ? import_chalk2.default.red : f.severity === "high" ? import_chalk2.default.yellow : import_chalk2.default.gray;
|
|
5303
|
+
console.log(color(` [${f.severity.toUpperCase()}] ${f.title}`));
|
|
5304
|
+
if (options.verbose) console.log(import_chalk2.default.gray(` ${f.description}`));
|
|
5305
|
+
});
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5308
|
+
if (options.app) {
|
|
5309
|
+
const result = await scanVibeApp({ appUrl: options.app });
|
|
5310
|
+
totalFindings += result.findings.length;
|
|
5311
|
+
criticalOrHigh += result.findings.filter(
|
|
5312
|
+
(f) => f.severity === "critical" || f.severity === "high"
|
|
5313
|
+
).length;
|
|
5314
|
+
if (!isJSON) {
|
|
5315
|
+
spinner?.stop();
|
|
5316
|
+
console.log(import_chalk2.default.bold(`
|
|
5317
|
+
App: ${options.app}`));
|
|
5318
|
+
console.log(import_chalk2.default.gray(` Platform detected: ${result.detectedPlatform}`));
|
|
5319
|
+
console.log(import_chalk2.default.gray(` Bundles analyzed: ${result.bundlesAnalyzed}`));
|
|
5320
|
+
if (result.errors.length > 0) {
|
|
5321
|
+
result.errors.forEach((e) => console.log(import_chalk2.default.yellow(` \u26A0 ${e}`)));
|
|
5322
|
+
}
|
|
5323
|
+
result.findings.forEach((f) => {
|
|
5324
|
+
const color = f.severity === "critical" ? import_chalk2.default.red : f.severity === "high" ? import_chalk2.default.yellow : import_chalk2.default.gray;
|
|
5325
|
+
console.log(color(` [${f.severity.toUpperCase()}] ${f.title}`));
|
|
5326
|
+
if (options.verbose) {
|
|
5327
|
+
console.log(import_chalk2.default.gray(` ${f.description}`));
|
|
5328
|
+
if (f.evidence) console.log(import_chalk2.default.gray(` Evidence: ${f.evidence}`));
|
|
5329
|
+
}
|
|
5330
|
+
});
|
|
5331
|
+
}
|
|
5332
|
+
}
|
|
5333
|
+
if (!isJSON) {
|
|
5334
|
+
console.log("");
|
|
5335
|
+
if (totalFindings === 0) {
|
|
5336
|
+
console.log(import_chalk2.default.green(" BaaS scan complete \u2014 no secrets or misconfigurations found"));
|
|
5337
|
+
} else {
|
|
5338
|
+
console.log(
|
|
5339
|
+
import_chalk2.default.yellow(` BaaS scan complete \u2014 ${totalFindings} finding(s), ${criticalOrHigh} critical/high`)
|
|
5340
|
+
);
|
|
5341
|
+
}
|
|
5342
|
+
console.log("");
|
|
5343
|
+
}
|
|
5344
|
+
}
|
|
2929
5345
|
function mapMCPResultToApiPayload(result) {
|
|
2930
5346
|
let packageUrl;
|
|
2931
5347
|
if (result.server.command === "npx") {
|