├── .dockerignore
├── .env
├── .env.development
├── .eslintrc.cjs
├── .firebaserc
├── .gitignore
├── .prettierrc
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── firebase.json
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
    ├── 404.html
    ├── index.html
    ├── logo-v2-t.png
    ├── logo.jpg
    └── vite.svg
├── secrets-ninja-proxy
    ├── .gitignore
    ├── README.md
    ├── requirements.txt
    ├── secrets-ninja-proxy.py
    └── services
    │   ├── aws.py
    │   ├── gcp.py
    │   ├── mongodb.py
    │   ├── postgres.py
    │   └── rabbitmq.py
├── src
    ├── App.css
    ├── App.jsx
    ├── assets
    │   ├── logo-t.png
    │   ├── logo.jpg
    │   └── react.svg
    ├── components
    │   ├── copy_button.jsx
    │   ├── dashboard.jsx
    │   ├── footer.jsx
    │   ├── navbar.jsx
    │   ├── pages
    │   │   └── secrets_checker.jsx
    │   ├── request_window.jsx
    │   ├── requests.jsx
    │   ├── response_window.jsx
    │   ├── response_window
    │   │   ├── json_grid_view.jsx
    │   │   └── json_view.jsx
    │   ├── sidebar.jsx
    │   └── table.jsx
    ├── css
    │   └── json_theme.css
    ├── data
    │   └── detectors.json
    ├── index.css
    ├── main.jsx
    └── modules
    │   └── universal.jsx
├── tailwind.config.js
└── vite.config.js
/.dockerignore:
--------------------------------------------------------------------------------
 1 | node_modules
 2 | __pycache__
 3 | *.pyc
 4 | .git
 5 | dist
 6 | .env
 7 | *.log
 8 | Dockerfile
 9 | .dockerignore
10 | **/venv/
11 | venv/
12 | **/.firebase/
13 | .firebase/
14 | dist/
15 | public/
16 | .DS_Store
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | VITE_SECRETS_NINJA_PROXY_ENDPOINT=https://proxy.secrets.ninja
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | VITE_SECRETS_NINJA_PROXY_ENDPOINT=http://localhost:8001
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
 1 | module.exports = {
 2 |   root: true,
 3 |   env: { browser: true, es2020: true },
 4 |   extends: [
 5 |     'eslint:recommended',
 6 |     'plugin:react/recommended',
 7 |     'plugin:react/jsx-runtime',
 8 |     'plugin:react-hooks/recommended',
 9 |   ],
10 |   ignorePatterns: ['dist', '.eslintrc.cjs'],
11 |   parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 |   settings: { react: { version: '18.2' } },
13 |   plugins: ['react-refresh'],
14 |   rules: {
15 |     'react/jsx-no-target-blank': 'off',
16 |     'react-refresh/only-export-components': [
17 |       'warn',
18 |       { allowConstantExport: true },
19 |     ],
20 |   },
21 | };
22 | 
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 |   "projects": {
3 |     "default": "api-keys-checker"
4 |   }
5 | }
6 | 
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | # Logs
 2 | logs
 3 | *.log
 4 | npm-debug.log*
 5 | yarn-debug.log*
 6 | yarn-error.log*
 7 | pnpm-debug.log*
 8 | lerna-debug.log*
 9 | 
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | 
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | .github/dependabot.yml
26 | .firebase/hosting.ZGlzdA.cache
27 | firebase.json
28 | .firebaserc
29 | secrets-ninja.code-workspace
30 | .env
31 | .env.development
32 | *.env.development
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 |   "semi": true,
3 |   "singleQuote": true,
4 |   "printWidth": 80,
5 |   "tabWidth": 2,
6 |   "trailingComma": "es5",
7 |   "endOfLine": "auto"
8 | }
9 | 
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
 1 | # How to Contribute
 2 | 
 3 | ## Adding a new service
 4 | 
 5 | 1. Add the module to `src/data/detectors.json`. Refer the below example for OpenAI
 6 | 
 7 | ```json
 8 | {
 9 |   "OpenAI": {
10 |     "endpoints": {
11 |       "organizations": {
12 |         "label": "Get Organizations",
13 |         "curl": "curl -X GET 'https://api.openai.com/v1/organizations' -H 'Authorization: Bearer sk-xxxx'",
14 |         "request_url": "https://api.openai.com/v1/organizations",
15 |         "request_method": "GET"
16 |       },
17 |       "additional_endpoints": {}
18 |     },
19 |     "input_fields": {
20 |       "api_key": {
21 |         "type": "text",
22 |         "label": "Enter OpenAI API Key",
23 |         "placeholder": "sk-xxxxx-xxxxx-xxxxx-xxxxx"
24 |       },
25 |       "additional_input_fields": {}
26 |     }
27 |   }
28 | }
29 | ```
30 | 
31 | 2. Update `src/components/requests.jsx` with the request code for this service
32 | 
33 | ```js
34 | case 'OpenAI':
35 |     response = await fetch(endpointURL, {
36 |         method: requestMethod,
37 |         headers: {
38 |             'Authorization': `Bearer ${inputData.api_key}`,
39 |         }
40 |     });
41 |     break;
42 | ```
43 | 
44 | 3. **(OPTIONAL)** Add the service icon in `src/components/sidebar.jsx`
45 | 
46 | - Icon for services can be discovered at [https://react-icons.github.io/react-icons/](https://react-icons.github.io/react-icons/)
47 | - Adding Icon is optional as services with no specified icons already use a placeholder icon
48 | 
49 | ```js
50 | import { RiOpenaiFill } from "react-icons/ri";
51 | 
52 | let serviceIcons = {
53 |     ....,
54 |     OpenAI: RiOpenaiFill,
55 |     ....
56 | }
57 | ```
58 | 
59 | 4. **CORS Error Workarounds**
60 | 
61 | If the api can't be accessed from browser due to CORS, add the following to the `src/data/detectors.json`. This will auto enable the thingproxy.freeboard.io checkbox
62 | 
63 | ```json
64 | "alert": {
65 |       "alert_message": "Use the curl on your local machine to test creds privately. Or check the proxy box to use secrets-ninja-proxy to bypass CORS.",
66 |       "color": "failure"
67 |     }
68 | ```
69 | 
70 | ## Note
71 | The following [GPT bot](https://chatgpt.com/g/g-67d9165cb410819191d7b463e4f0a9a2-secrets-ninja-contribution-bot) can be used to generate somewhat functional modules for secrets.ninja
72 | - Output Modules generated using GPT may not be 100% correct and may require testing
73 | 
74 | 
75 | - I understand that the size of `src/data/detectors.json` is getting big, in future will be moving it to database or having individual JSON for each service.
76 | - I understand that creating a different switch case of each service is redundant, and a universal function can be created for most of these, but I wanted the code to be easily contributable as it can be.
77 | - Only contribute readonly endpoints, don't want to add any endpoints which allows adding, updating, pushing, or deletion of data, change of state on the service.
78 | 
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
 1 | FROM node:20-bullseye
 2 | 
 3 | # Install Python and build deps
 4 | RUN apt-get update && \
 5 |     apt-get install -y python3 python3-pip python3-venv gcc libpq-dev git && \
 6 |     rm -rf /var/lib/apt/lists/*
 7 | 
 8 | WORKDIR /app
 9 | 
10 | # Copy project files into the image
11 | COPY . .
12 | 
13 | # Install frontend deps
14 | RUN npm install
15 | 
16 | # Install backend deps
17 | WORKDIR /app/secrets-ninja-proxy
18 | RUN pip3 install --no-cache-dir -r requirements.txt
19 | 
20 | # Final config
21 | WORKDIR /app
22 | EXPOSE 5173 8001
23 | 
24 | CMD bash -c "npm run dev -- --host 0.0.0.0 & \
25 |     cd /app/secrets-ninja-proxy && \
26 |     python3 -m uvicorn secrets-ninja-proxy:app --host 0.0.0.0 --port 8001 & \
27 |     wait -n"
28 | 
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2024 NIKHIL PANWAR
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 | 
 5 | [](https://github.com/NikhilPanwar/secrets-ninja)
 6 | [](https://github.com/NikhilPanwar/secrets-ninja/blob/master/LICENSE)
 7 | [](https://github.com/NikhilPanwar/secrets-ninja/graphs/contributors)
 8 | [](https://github.com/NikhilPanwar/secrets-ninja)
 9 | 
10 |   
Secrets Ninja 
11 | 
12 | [secrets.ninja](https://secrets.ninja) is a tool for validating API keys and credentials discovered during pentesting & bug bounty hunting.
13 | 
It proivdes a unified interface for testing these keys across SaaS, Databases, Cloud Providers & services
14 | 
15 | 
 
16 | 
17 | ## Features
18 | 
19 | - **Multiple Service Support:** Secrets Ninja supports a wide range of services, each with a dedicated module for validating API keys.
20 | - **Extensible Design:** The project is designed to be easily extensible, allowing for the addition of new modules for other services.
21 | - **User-Friendly Interface:** A simple and intuitive interface for inputting API keys and making requests.
22 | - **Clear Feedback:** Provides clear feedback on the validity of the keys and any information retrieved from the API calls.
23 | 
24 | ## Getting Started
25 | 
26 | To get started with Secrets Ninja, install the dependencies and run the development server.
27 | 
28 | - Install dependencies using below command
29 | 
30 | ```bash
31 | $ npm install
32 | $ npm run dev
33 | ```
34 | 
35 | Or Run Using Docker, Including the Secrets Ninja Proxy for testing AWS, MongoDB creds privately
36 | ```
37 | docker run -p 5173:5173 -p 8001:8001 secretsninja/secrets-ninja:latest
38 | ```
39 | 
40 | Access the development server at [http://localhost:5173/](http://localhost:5173/)
41 | 
42 | ## Contributing
43 | 
44 | Contributions are welcome, particularly new modules for validating API keys on additional services. 
45 | Please note that due to CORS restrictions or in case of Cloud Creds which requires SDK, CLI tools, some APIs can't be accessed using frontend JS only. In such cases, the project provides workaround using secrets-ninja-proxy module.
46 | 
47 | Interested in contributing to the project? [Here's](CONTRIBUTING.md) how you can get started.
48 | 
49 | ## Disclaimer
50 | 
51 | This tool is intended for ethical use only. It is the user's responsibility to comply with all applicable laws and terms of service when using this tool.
52 | 
53 | ## License
54 | 
55 | Secrets Ninja is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
56 | 
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "hosting": {
 3 |     "public": "dist",
 4 |     "ignore": [
 5 |       "firebase.json",
 6 |       "**/.*",
 7 |       "**/node_modules/**"
 8 |     ],
 9 |     "rewrites": [
10 |       {
11 |         "source": "**",
12 |         "destination": "/index.html"
13 |       }
14 |     ]
15 |   }
16 | }
17 | 
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |      
 5 |      
 6 |      
 7 | 
 8 |     Secrets Ninja 
 9 |      
10 |      
11 |      
12 |      
13 | 
14 |     
15 |      
16 |      
17 |      
18 |      
19 |      
20 |      
21 |      
22 |   
23 |   
24 |     
25 |     
26 |   
27 | 
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "secrets-ninja",
 3 |   "private": true,
 4 |   "version": "0.0.0",
 5 |   "type": "module",
 6 |   "scripts": {
 7 |     "dev": "vite",
 8 |     "build": "vite build",
 9 |     "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 |     "preview": "vite preview",
11 |     "format": "prettier --write ."
12 |   },
13 |   "dependencies": {
14 |     "@floating-ui/core": "^1.6.9",
15 |     "@floating-ui/dom": "^1.6.13",
16 |     "@redheadphone/react-json-grid": "^0.9.4",
17 |     "autoprefixer": "^10.4.17",
18 |     "flowbite-react": "^0.7.2",
19 |     "hamburger-react": "^2.5.0",
20 |     "postcss": "^8.5.3",
21 |     "react": "^18.2.0",
22 |     "react-dom": "^18.2.0",
23 |     "react-icons": "^5.5.0",
24 |     "react-json-pretty": "^2.2.0",
25 |     "react-router-dom": "^6.22.1",
26 |     "react-syntax-highlighter": "^15.6.1",
27 |     "react-table": "^7.8.0",
28 |     "react-use": "^17.6.0",
29 |     "tailwindcss": "^3.4.1"
30 |   },
31 |   "devDependencies": {
32 |     "@types/react": "^18.2.56",
33 |     "@types/react-dom": "^18.2.19",
34 |     "@typescript-eslint/eslint-plugin": "^8.20.0",
35 |     "@typescript-eslint/parser": "^8.20.0",
36 |     "@vitejs/plugin-react": "^4.2.1",
37 |     "eslint": "^8.57.1",
38 |     "eslint-config-prettier": "^10.0.1",
39 |     "eslint-plugin-jsx-a11y": "^6.10.2",
40 |     "eslint-plugin-prettier": "^5.2.1",
41 |     "eslint-plugin-react": "^7.37.4",
42 |     "eslint-plugin-react-hooks": "^4.6.2",
43 |     "eslint-plugin-react-refresh": "^0.4.5",
44 |     "prettier": "^3.4.2",
45 |     "vite": "^5.1.4"
46 |   }
47 | }
48 | 
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 |   plugins: {
3 |     tailwindcss: {},
4 |     autoprefixer: {},
5 |   },
6 | };
7 | 
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |      
 5 |      
 6 |     Page Not Found 
 7 | 
 8 |     
79 |   
80 |   
81 |     
82 |       
404 
83 |       
Page Not Found 
84 |       
85 |         The specified file was not found on this website. Please check the URL
86 |         for mistakes and try again.
87 |       
88 |       
Why am I seeing this? 
89 |       
90 |         This page was generated by the Firebase Command-Line Interface. To
91 |         modify it, edit the 404.html file in your project's
92 |         configured public directory.
93 |       
94 |     
 
95 |   
96 | 
97 | 
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
  1 | 
  2 | 
  3 |   
  4 |      
  5 |      
  6 |     Welcome to Firebase Hosting 
  7 | 
  8 |     
  9 |     
 10 |     
 11 |     
 12 |     
 16 |     
 20 |     
 24 |     
 28 |     
 29 |     
 33 |     
 37 |     
 41 |     
 45 |     
 46 | 
 47 |     
112 |   
113 |   
114 |     
115 |       
Welcome 
116 |       
Firebase Hosting Setup Complete 
117 |       
118 |         You're seeing this because you've successfully setup Firebase Hosting.
119 |         Now it's time to go build something extraordinary!
120 |       
121 |       
Open Hosting Documentation 
124 |     
 
125 |     Firebase SDK Loading…
126 | 
127 |     
166 |   
167 | 
168 | 
--------------------------------------------------------------------------------
/public/logo-v2-t.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NikhilPanwar/secrets-ninja/9db8616d4024cfe7329b439818769f94b8cde70a/public/logo-v2-t.png
--------------------------------------------------------------------------------
/public/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NikhilPanwar/secrets-ninja/9db8616d4024cfe7329b439818769f94b8cde70a/public/logo.jpg
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |  
--------------------------------------------------------------------------------
/secrets-ninja-proxy/.gitignore:
--------------------------------------------------------------------------------
1 | *pycache*
2 | *venv*
--------------------------------------------------------------------------------
/secrets-ninja-proxy/README.md:
--------------------------------------------------------------------------------
 1 | # secrets-ninja-proxy
 2 | Proxy Server for secrets ninja, use it for bypassing CORS, verifying complex keys like AWS, DB creds
 3 | 
 4 | # Install Dependencies
 5 | ```
 6 | python3 -m pip install -r requirements.txt
 7 | ```
 8 | 
 9 | # Run
10 | ```
11 | python3 -m uvicorn secrets-ninja-proxy:app --reload --host 0.0.0.0 --port 8001 --reload
12 | ```
--------------------------------------------------------------------------------
/secrets-ninja-proxy/requirements.txt:
--------------------------------------------------------------------------------
 1 | fastapi
 2 | uvicorn
 3 | boto3
 4 | requests
 5 | pymongo
 6 | pika
 7 | psycopg2
 8 | google-auth
 9 | google-auth-oauthlib
10 | google-auth-httplib2
11 | google-api-python-client
12 | google-cloud-storage
13 | google-cloud-iam
14 | 
--------------------------------------------------------------------------------
/secrets-ninja-proxy/secrets-ninja-proxy.py:
--------------------------------------------------------------------------------
 1 | from fastapi import FastAPI, Request
 2 | from fastapi.responses import JSONResponse
 3 | from fastapi.middleware.cors import CORSMiddleware
 4 | import requests
 5 | from urllib.parse import unquote, parse_qsl
 6 | import json
 7 | from services import aws, mongodb, rabbitmq, postgres, gcp
 8 | 
 9 | app = FastAPI()
10 | 
11 | app.include_router(aws.router, prefix="/aws", tags=["aws"])
12 | app.include_router(mongodb.router, prefix="/mongodb", tags=["mongodb"])
13 | app.include_router(rabbitmq.router, prefix="/rabbitmq", tags=["rabbitmq"])
14 | app.include_router(postgres.router, prefix="/postgres", tags=["postgres"])
15 | app.include_router(gcp.router, prefix="/gcp", tags=["gcp"])
16 | 
17 | app.add_middleware(
18 |     CORSMiddleware,
19 |     allow_origins=["*"],
20 |     allow_methods=["*"],
21 |     allow_headers=["*"],
22 | )
23 | 
24 | def clean_headers(browser_headers):
25 |     values_to_delete = [
26 |         "host", "Host", "content-length", "Content-Length", "Content-Type", "content-type", "connection",
27 |         "Connection", "accept-encoding", "Accept-Encoding", "accept",
28 |         "Accept", "origin", "Origin", "referer", "Referer", "user-agent", "User-Agent",
29 |         "sec-ch-ua-platform", "sec-ch-ua-mobile", "sec-fetch-dest", "sec-fetch-mode",
30 |         "accept-language", "sec-fetch-site", "sec-fetch-user", "sec-ch-ua", "sec-ch-ua-arch", "sec-gpc",
31 |     ]
32 |     for value in values_to_delete:
33 |         browser_headers.pop(value, None)
34 |     return browser_headers
35 | 
36 | def make_request(url, headers, method, json_body=None):
37 |     url = unquote(url)
38 |     if method == "GET":
39 |         print("Making GET request to", url)
40 |         response = requests.get(url, headers=headers)
41 |         try:
42 |             return json.loads(response.text), response.status_code
43 |         except:
44 |             return {"response": response.text}, response.status_code
45 |     elif method == "POST":
46 |         if 'x-www-form-urlencoded' in json.dumps(headers):
47 |             response = requests.post(url, headers=headers, data=json_body)
48 |         else:
49 |             response = requests.post(url, headers=headers, json=json_body)
50 |         return response.json(), response.status_code
51 | 
52 | @app.options("/{full_path:path}")
53 | async def handle_options(full_path: str):
54 |     return JSONResponse(status_code=200, content={"message": "OK"})
55 | 
56 | @app.api_route("/fetch/{rest_of_path:path}", methods=["POST", "GET"])
57 | async def fetch_handler(request: Request, rest_of_path: str):
58 |     try:
59 |         json_body = await request.json()
60 |     except:
61 |         json_body = {}
62 |     method = request.method
63 |     headers = json_body.get("proxied_data", {}).get("headers", {})
64 |     if headers == {}:
65 |         real_headers = dict(request.headers)
66 |         headers = clean_headers(real_headers)
67 |     full_url = str(request.url).replace(str(request.base_url) + "fetch/", "")
68 |     body = json_body.get("body", None)
69 |     if body:
70 |         parsed_body = dict(parse_qsl(body)) if isinstance(body, str) else body
71 |     else:
72 |         parsed_body = {}
73 | 
74 |     response, status_code = make_request(full_url, headers, method, parsed_body)
75 |     return JSONResponse(status_code=status_code, content=response)
76 | 
77 | if __name__ == "__main__":
78 |     import uvicorn
79 |     uvicorn.run("main:app", host="0.0.0.0", port=8001, reload=True)
--------------------------------------------------------------------------------
/secrets-ninja-proxy/services/aws.py:
--------------------------------------------------------------------------------
  1 | from fastapi import APIRouter, Request, HTTPException
  2 | import boto3
  3 | from botocore.exceptions import ClientError
  4 | from datetime import datetime
  5 | 
  6 | router = APIRouter()
  7 | 
  8 | @router.post("/list_buckets")
  9 | async def list_buckets(request: Request):
 10 |     data = await request.json()
 11 |     aws_access_key = data.get("aws_access_key")
 12 |     aws_secret_key = data.get("aws_secret_key")
 13 |     region = data.get("region", "us-east-1")
 14 |     if not aws_access_key or not aws_secret_key:
 15 |         raise HTTPException(status_code=400, detail="Missing AWS credentials")
 16 |     try:
 17 |         s3_client = boto3.client(
 18 |             "s3",
 19 |             aws_access_key_id=aws_access_key,
 20 |             aws_secret_access_key=aws_secret_key,
 21 |             region_name=region
 22 |         )
 23 |         response = s3_client.list_buckets()
 24 |         buckets = [bucket["Name"] for bucket in response.get("Buckets", [])]
 25 |         return {"buckets": buckets}
 26 |     except ClientError as e:
 27 |         raise HTTPException(status_code=500, detail=str(e))
 28 | 
 29 | @router.post("/list_ec2_instances")
 30 | async def list_ec2_instances(request: Request):
 31 |     data = await request.json()
 32 |     aws_access_key = data.get("aws_access_key")
 33 |     aws_secret_key = data.get("aws_secret_key")
 34 |     region = data.get("region", "us-east-1")
 35 |     if not aws_access_key or not aws_secret_key:
 36 |         raise HTTPException(status_code=400, detail="Missing AWS credentials")
 37 |     try:
 38 |         ec2_client = boto3.client(
 39 |             "ec2",
 40 |             aws_access_key_id=aws_access_key,
 41 |             aws_secret_access_key=aws_secret_key,
 42 |             region_name=region
 43 |         )
 44 |         response = ec2_client.describe_instances()
 45 |         instance_ids = []
 46 |         for reservation in response.get("Reservations", []):
 47 |             for instance in reservation.get("Instances", []):
 48 |                 instance_ids.append(instance.get("InstanceId"))
 49 |         return {"instances": instance_ids}
 50 |     except ClientError as e:
 51 |         raise HTTPException(status_code=500, detail=str(e))
 52 | 
 53 | @router.post("/get_cost_and_usage")
 54 | async def get_cost_and_usage(request: Request):
 55 |     data = await request.json()
 56 |     aws_access_key = data.get("aws_access_key")
 57 |     aws_secret_key = data.get("aws_secret_key")
 58 |     region = "us-east-1"  # Cost Explorer only works in us-east-1
 59 |     if not aws_access_key or not aws_secret_key:
 60 |         raise HTTPException(status_code=400, detail="Missing AWS credentials")
 61 |     try:
 62 |         start = datetime.today().replace(day=1).strftime("%Y-%m-%d")
 63 |         end = datetime.today().strftime("%Y-%m-%d")
 64 |         ce_client = boto3.client(
 65 |             "ce",
 66 |             aws_access_key_id=aws_access_key,
 67 |             aws_secret_access_key=aws_secret_key,
 68 |             region_name=region
 69 |         )
 70 |         response = ce_client.get_cost_and_usage(
 71 |             TimePeriod={
 72 |                 "Start": start,
 73 |                 "End": end
 74 |             },
 75 |             Granularity="MONTHLY",
 76 |             Metrics=["UnblendedCost"]
 77 |         )
 78 |         return {"cost_and_usage": response}
 79 |     except ClientError as e:
 80 |         raise HTTPException(status_code=500, detail=str(e))
 81 | 
 82 | @router.post("/get_service_cost_and_usage")
 83 | async def get_service_cost_and_usage(request: Request):
 84 |     data = await request.json()
 85 |     aws_access_key = data.get("aws_access_key")
 86 |     aws_secret_key = data.get("aws_secret_key")
 87 |     region = "us-east-1"  # Cost Explorer works only in us-east-1
 88 |     if not aws_access_key or not aws_secret_key:
 89 |         raise HTTPException(status_code=400, detail="Missing AWS credentials")
 90 |     try:
 91 |         start = datetime.today().replace(day=1).strftime("%Y-%m-%d")
 92 |         end = datetime.today().strftime("%Y-%m-%d")
 93 |         ce_client = boto3.client(
 94 |             "ce",
 95 |             aws_access_key_id=aws_access_key,
 96 |             aws_secret_access_key=aws_secret_key,
 97 |             region_name=region
 98 |         )
 99 |         response = ce_client.get_cost_and_usage(
100 |             TimePeriod={
101 |                 "Start": start,
102 |                 "End": end
103 |             },
104 |             Granularity="MONTHLY",
105 |             Metrics=["UnblendedCost"],
106 |             GroupBy=[
107 |                 {
108 |                     'Type': 'DIMENSION',
109 |                     'Key': 'SERVICE'
110 |                 }
111 |             ]
112 |         )
113 |         return {"cost_and_usage": response.get('ResultsByTime', [])}
114 |     except ClientError as e:
115 |         raise HTTPException(status_code=500, detail=str(e))
116 | 
117 | @router.post("/list_account_aliases")
118 | async def list_account_aliases(request: Request):
119 |     data = await request.json()
120 |     aws_access_key = data.get("aws_access_key")
121 |     aws_secret_key = data.get("aws_secret_key")
122 |     if not aws_access_key or not aws_secret_key:
123 |         raise HTTPException(status_code=400, detail="Missing AWS credentials")
124 |     try:
125 |         iam_client = boto3.client(
126 |             "iam",
127 |             aws_access_key_id=aws_access_key,
128 |             aws_secret_access_key=aws_secret_key
129 |         )
130 |         response = iam_client.list_account_aliases()
131 |         return {"account_aliases": response.get("AccountAliases", [])}
132 |     except ClientError as e:
133 |         raise HTTPException(status_code=500, detail=str(e))
134 | 
135 | @router.post("/list_hosted_zones")
136 | async def list_hosted_zones(request: Request):
137 |     data = await request.json()
138 |     aws_access_key = data.get("aws_access_key")
139 |     aws_secret_key = data.get("aws_secret_key")
140 |     if not aws_access_key or not aws_secret_key:
141 |         raise HTTPException(status_code=400, detail="Missing AWS credentials")
142 |     try:
143 |         route53_client = boto3.client(
144 |             "route53",
145 |             aws_access_key_id=aws_access_key,
146 |             aws_secret_access_key=aws_secret_key
147 |         )
148 |         response = route53_client.list_hosted_zones()
149 |         zones = [zone["Name"] for zone in response.get("HostedZones", [])]
150 |         return {"hosted_zones": zones}
151 |     except ClientError as e:
152 |         raise HTTPException(status_code=500, detail=str(e))
153 | 
154 | @router.post("/list_roles")
155 | async def list_roles(request: Request):
156 |     data = await request.json()
157 |     aws_access_key = data.get("aws_access_key")
158 |     aws_secret_key = data.get("aws_secret_key")
159 |     if not aws_access_key or not aws_secret_key:
160 |         raise HTTPException(status_code=400, detail="Missing AWS credentials")
161 |     try:
162 |         iam_client = boto3.client(
163 |             "iam",
164 |             aws_access_key_id=aws_access_key,
165 |             aws_secret_access_key=aws_secret_key
166 |         )
167 |         response = iam_client.list_roles()
168 |         roles = [role["RoleName"] for role in response.get("Roles", [])]
169 |         return {"roles": roles}
170 |     except ClientError as e:
171 |         raise HTTPException(status_code=500, detail=str(e))
172 | 
173 | @router.post("/get_caller_identity")
174 | async def get_caller_identity(request: Request):
175 |     data = await request.json()
176 |     aws_access_key = data.get("aws_access_key")
177 |     aws_secret_key = data.get("aws_secret_key")
178 |     region = data.get("region", "us-east-1")
179 |     if not aws_access_key or not aws_secret_key:
180 |         raise HTTPException(status_code=400, detail="Missing AWS credentials")
181 |     try:
182 |         sts_client = boto3.client(
183 |             "sts",
184 |             aws_access_key_id=aws_access_key,
185 |             aws_secret_access_key=aws_secret_key,
186 |             region_name=region
187 |         )
188 |         response = sts_client.get_caller_identity()
189 |         return {"caller_identity": response}
190 |     except ClientError as e:
191 |         raise HTTPException(status_code=500, detail=str(e))
192 | 
193 | @router.post("/describe_organization")
194 | async def describe_organization(request: Request):
195 |     data = await request.json()
196 |     aws_access_key = data.get("aws_access_key")
197 |     aws_secret_key = data.get("aws_secret_key")
198 |     region = data.get("region", "us-east-1")
199 |     if not aws_access_key or not aws_secret_key:
200 |         raise HTTPException(status_code=400, detail="Missing AWS credentials")
201 |     try:
202 |         org_client = boto3.client(
203 |             "organizations",
204 |             aws_access_key_id=aws_access_key,
205 |             aws_secret_access_key=aws_secret_key,
206 |             region_name=region
207 |         )
208 |         response = org_client.describe_organization()
209 |         return {"organization": response}
210 |     except ClientError as e:
211 |         raise HTTPException(status_code=500, detail=str(e))
212 | 
213 | @router.post("/get_contact_information")
214 | async def get_contact_information(request: Request):
215 |     data = await request.json()
216 |     aws_access_key = data.get("aws_access_key")
217 |     aws_secret_key = data.get("aws_secret_key")
218 |     region = data.get("region", "us-east-1")
219 |     if not aws_access_key or not aws_secret_key:
220 |         raise HTTPException(status_code=400, detail="Missing AWS credentials")
221 |     try:
222 |         account_client = boto3.client(
223 |             "account",
224 |             aws_access_key_id=aws_access_key,
225 |             aws_secret_access_key=aws_secret_key,
226 |             region_name=region
227 |         )
228 |         response = account_client.get_contact_information()
229 |         return {"contact_information": response}
230 |     except ClientError as e:
231 |         raise HTTPException(status_code=500, detail=str(e))
232 | 
--------------------------------------------------------------------------------
/secrets-ninja-proxy/services/gcp.py:
--------------------------------------------------------------------------------
 1 | from fastapi import APIRouter, Request, HTTPException
 2 | from google.oauth2 import service_account
 3 | from googleapiclient.discovery import build
 4 | from google.cloud import storage
 5 | from google.cloud import iam
 6 | import json
 7 | 
 8 | router = APIRouter()
 9 | 
10 | @router.post("/list_projects")
11 | async def list_projects(request: Request):
12 |     data = await request.json()
13 |     gcp_creds = data.get("gcp_creds")
14 |     gcp_creds = json.loads(gcp_creds)
15 |     if not gcp_creds:
16 |         raise HTTPException(status_code=400, detail="Missing GCP credentials")
17 |     try:
18 |         credentials = service_account.Credentials.from_service_account_info(gcp_creds)
19 |         service = build('cloudresourcemanager', 'v1', credentials=credentials)
20 |         request = service.projects().list()
21 |         response = request.execute()
22 |         projects = response.get('projects', [])
23 |         return {"projects": projects}
24 |     except Exception as e:
25 |         raise HTTPException(status_code=500, detail=str(e))
26 | 
27 | @router.post("/list_compute_instances")
28 | async def list_compute_instances(request: Request):
29 |     data = await request.json()
30 |     gcp_creds = data.get("gcp_creds")
31 |     gcp_creds = json.loads(gcp_creds)
32 |     project_id = data.get("project_id")
33 |     zone = data.get("zone")
34 |     if not gcp_creds or not project_id or not zone:
35 |         raise HTTPException(status_code=400, detail="Missing GCP credentials, project ID, or zone")
36 |     try:
37 |         credentials = service_account.Credentials.from_service_account_info(gcp_creds)
38 |         service = build('compute', 'v1', credentials=credentials)
39 |         request = service.instances().list(project=project_id, zone=zone)
40 |         response = request.execute()
41 |         instances = response.get('items', [])
42 |         return {"instances": instances}
43 |     except Exception as e:
44 |         raise HTTPException(status_code=500, detail=str(e))
45 | 
46 | @router.post("/list_buckets")
47 | async def list_buckets(request: Request):
48 |     data = await request.json()
49 |     gcp_creds = data.get("gcp_creds")
50 |     gcp_creds = json.loads(gcp_creds)
51 |     if not gcp_creds:
52 |         raise HTTPException(status_code=400, detail="Missing GCP credentials")
53 |     try:
54 |         credentials = service_account.Credentials.from_service_account_info(gcp_creds)
55 |         client = storage.Client(credentials=credentials)
56 |         buckets = list(client.list_buckets())
57 |         bucket_names = [bucket.name for bucket in buckets]
58 |         return {"buckets": bucket_names}
59 |     except Exception as e:
60 |         raise HTTPException(status_code=500, detail=str(e))
61 | 
62 | @router.post("/list_iam_users")
63 | async def list_iam_users(request: Request):
64 |     data = await request.json()
65 |     gcp_creds = data.get("gcp_creds")
66 |     gcp_creds = json.loads(gcp_creds)
67 |     if not gcp_creds:
68 |         raise HTTPException(status_code=400, detail="Missing GCP credentials")
69 |     try:
70 |         credentials = service_account.Credentials.from_service_account_info(gcp_creds)
71 |         client = iam.IAMClient(credentials=credentials)
72 |         users = list(client.list_service_accounts())
73 |         user_emails = [user.email for user in users]
74 |         return {"iam_users": user_emails}
75 |     except Exception as e:
76 |         raise HTTPException(status_code=500, detail=str(e))
77 | 
--------------------------------------------------------------------------------
/secrets-ninja-proxy/services/mongodb.py:
--------------------------------------------------------------------------------
 1 | from fastapi import APIRouter, Request, HTTPException
 2 | from pymongo import MongoClient
 3 | from pymongo.errors import ConnectionFailure
 4 | 
 5 | router = APIRouter()
 6 |     
 7 | @router.post("/list_databases")
 8 | async def list_databases(request: Request):
 9 |     data = await request.json()
10 |     mongodb_uri = data.get("mongodb_uri")
11 |     if not mongodb_uri:
12 |         raise HTTPException(status_code=400, detail="Missing MongoDB URI")
13 |     try:
14 |         client = MongoClient(mongodb_uri)
15 |         db_list = client.list_database_names()
16 |         dbs_info = []
17 |         for db_name in db_list:
18 |             db = client[db_name]
19 |             stats = db.command("dbstats")
20 |             collections = db.list_collection_names()
21 |             num_collections = len(collections)
22 |             total_indexes = 0
23 |             for coll in collections:
24 |                 indexes = db[coll].index_information()
25 |                 total_indexes += len(indexes)
26 |             dbs_info.append({
27 |                 "dbname": db_name,
28 |                 "dbsize": stats.get("dataSize", 0),
29 |                 "number_of_collections": num_collections,
30 |                 "number_of_indexes": total_indexes
31 |             })
32 |         return {"databases": dbs_info}
33 |     except Exception as e:
34 |         raise HTTPException(status_code=500, detail=str(e))
35 | 
36 | @router.post("/list_db_collections")
37 | async def list_db_collections(request: Request):
38 |     data = await request.json()
39 |     mongodb_uri = data.get("mongodb_uri")
40 |     database_name = data.get("database")
41 |     if not mongodb_uri or not database_name:
42 |         raise HTTPException(status_code=400, detail="Missing MongoDB URI or database name")
43 |     try:
44 |         client = MongoClient(mongodb_uri)
45 |         db = client[database_name]
46 |         collections = db.list_collection_names()
47 |         collections_info = []
48 |         for coll in collections:
49 |             stats = db.command("collstats", coll)
50 |             collections_info.append({
51 |                 "collection_name": coll,
52 |                 "size": stats.get("size", 0),
53 |                 "doc_count": stats.get("count", 0),
54 |                 "avg_doc_size": stats.get("avgObjSize", 0),
55 |                 "index_count": stats.get("nindexes", 0)
56 |             })
57 |         return {"collections": collections_info}
58 |     except Exception as e:
59 |         raise HTTPException(status_code=500, detail=str(e))
60 | 
61 | @router.post("/list_records")
62 | async def list_records(request: Request):
63 |     data = await request.json()
64 |     mongodb_uri = data.get("mongodb_uri")
65 |     database_name = data.get("database")
66 |     collection_name = data.get("collection")
67 |     if not mongodb_uri or not database_name or not collection_name:
68 |         raise HTTPException(status_code=400, detail="Missing MongoDB URI, database, or collection name")
69 |     try:
70 |         client = MongoClient(mongodb_uri)
71 |         db = client[database_name]
72 |         collection = db[collection_name]
73 |         records = list(collection.find().limit(5))
74 |         # Convert ObjectId to string for JSON serialization if needed
75 |         for record in records:
76 |             if "_id" in record:
77 |                 record["_id"] = str(record["_id"])
78 |         return {"records": records}
79 |     except Exception as e:
80 |         raise HTTPException(status_code=500, detail=str(e))
81 | 
--------------------------------------------------------------------------------
/secrets-ninja-proxy/services/postgres.py:
--------------------------------------------------------------------------------
  1 | from fastapi import APIRouter, Request, HTTPException
  2 | import psycopg2
  3 | import psycopg2.extras
  4 | from urllib.parse import urlparse, parse_qs
  5 | 
  6 | router = APIRouter()
  7 | 
  8 | def connect_to_postgres(postgres_uri):
  9 |     def try_connect(uri):
 10 |         try:
 11 |             return psycopg2.connect(uri)
 12 |         except psycopg2.OperationalError:
 13 |             return None
 14 | 
 15 |     conn = try_connect(postgres_uri)
 16 |     if conn:
 17 |         return conn
 18 | 
 19 |     try:
 20 |         parsed_uri = urlparse(postgres_uri)
 21 |         query_params = parse_qs(parsed_uri.query)
 22 | 
 23 |         current_sslmode = query_params.get('sslmode', ['require'])[0]
 24 | 
 25 |         # Flip sslmode
 26 |         new_sslmode = 'disable' if current_sslmode == 'require' else 'require'
 27 | 
 28 |         new_params = {k: v[0] for k, v in query_params.items() if k != 'sslmode'}
 29 |         new_params['sslmode'] = new_sslmode
 30 |         query_string = '&'.join([f"{k}={v}" for k, v in new_params.items()])
 31 | 
 32 |         new_uri = f"{parsed_uri.scheme}://{parsed_uri.netloc}{parsed_uri.path}?{query_string}"
 33 | 
 34 |         conn = try_connect(new_uri)
 35 |         if conn:
 36 |             return conn
 37 | 
 38 |         raise HTTPException(status_code=500, detail="Failed to connect to PostgreSQL with both sslmode options")
 39 | 
 40 |     except Exception as e:
 41 |         raise HTTPException(status_code=500, detail=f"Failed to connect to PostgreSQL: {str(e)}")
 42 |             
 43 | @router.post("/list_databases")
 44 | async def list_databases(request: Request):
 45 |     data = await request.json()
 46 |     postgres_uri = data.get("postgres_uri")
 47 |     
 48 |     if not postgres_uri:
 49 |         raise HTTPException(status_code=400, detail="Missing PostgreSQL URI")
 50 |     
 51 |     try:
 52 |         conn = connect_to_postgres(postgres_uri)
 53 |         cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
 54 |         
 55 |         query = """
 56 |         SELECT 
 57 |             d.datname as database_name,
 58 |             pg_database_size(d.datname) as database_size,
 59 |             pg_size_pretty(pg_database_size(d.datname)) as pretty_size
 60 |         FROM 
 61 |             pg_database d
 62 |         WHERE 
 63 |             d.datistemplate = false
 64 |         ORDER BY 
 65 |             pg_database_size(d.datname) DESC;
 66 |         """
 67 |         
 68 |         cursor.execute(query)
 69 |         databases = []
 70 |         
 71 |         for row in cursor.fetchall():
 72 |             databases.append({
 73 |                 "dbname": row["database_name"],
 74 |                 "dbsize": row["database_size"],
 75 |                 "pretty_size": row["pretty_size"]
 76 |             })
 77 |         
 78 |         cursor.close()
 79 |         conn.close()
 80 |         
 81 |         return {"databases": databases}
 82 |     
 83 |     except Exception as e:
 84 |         raise HTTPException(status_code=500, detail=str(e))
 85 | 
 86 | def modify_uri_database(postgres_uri, database_name):
 87 |     parsed_uri = urlparse(postgres_uri)
 88 |     
 89 |     if parsed_uri.path and parsed_uri.path != "/":
 90 |         path_parts = parsed_uri.path.split('/')
 91 |         path_parts[-1] = database_name
 92 |         new_path = '/'.join(path_parts)
 93 |     else:
 94 |         new_path = f"/{database_name}"
 95 |     
 96 |     netloc = parsed_uri.netloc
 97 |     scheme = parsed_uri.scheme
 98 |     query = parsed_uri.query
 99 |     
100 |     new_uri = f"{scheme}://{netloc}{new_path}"
101 |     if query:
102 |         new_uri += f"?{query}"
103 |     
104 |     return new_uri
105 | 
106 | @router.post("/list_db_tables")
107 | async def list_db_tables(request: Request):
108 |     data = await request.json()
109 |     postgres_uri = data.get("postgres_uri")
110 |     database_name = data.get("database")
111 |     
112 |     if not postgres_uri or not database_name:
113 |         raise HTTPException(status_code=400, detail="Missing PostgreSQL URI or database name")
114 |     
115 |     try:
116 |         new_uri = modify_uri_database(postgres_uri, database_name)
117 |         conn = connect_to_postgres(new_uri)
118 |         cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
119 |         
120 |         query = """
121 |         SELECT 
122 |             t.table_schema,
123 |             t.table_name,
124 |             pg_relation_size(quote_ident(t.table_schema) || '.' || quote_ident(t.table_name)) as table_size,
125 |             pg_size_pretty(pg_relation_size(quote_ident(t.table_schema) || '.' || quote_ident(t.table_name))) as pretty_size,
126 |             (SELECT count(*) FROM information_schema.columns WHERE table_schema = t.table_schema AND table_name = t.table_name) as column_count,
127 |             (SELECT count(*) FROM pg_indexes WHERE schemaname = t.table_schema AND tablename = t.table_name) as index_count,
128 |             coalesce(s.n_live_tup, 0) as estimated_row_count
129 |         FROM 
130 |             information_schema.tables t
131 |         LEFT JOIN 
132 |             pg_stat_user_tables s ON s.schemaname = t.table_schema AND s.relname = t.table_name
133 |         WHERE 
134 |             t.table_schema NOT IN ('pg_catalog', 'information_schema')
135 |             AND t.table_type = 'BASE TABLE'
136 |         ORDER BY 
137 |             t.table_schema, t.table_name;
138 |         """
139 |         
140 |         cursor.execute(query)
141 |         tables = []
142 |         
143 |         for row in cursor.fetchall():
144 |             tables.append({
145 |                 "schema": row["table_schema"],
146 |                 "table_name": row["table_name"],
147 |                 "size": row["table_size"],
148 |                 "pretty_size": row["pretty_size"],
149 |                 "column_count": row["column_count"],
150 |                 "index_count": row["index_count"],
151 |                 "estimated_row_count": row["estimated_row_count"]
152 |             })
153 |         
154 |         cursor.close()
155 |         conn.close()
156 |         
157 |         return {"tables": tables}
158 |     
159 |     except Exception as e:
160 |         raise HTTPException(status_code=500, detail=str(e))
161 | 
162 | @router.post("/list_records")
163 | async def list_records(request: Request):
164 |     data = await request.json()
165 |     postgres_uri = data.get("postgres_uri")
166 |     database_name = data.get("database")
167 |     table_name = data.get("table")
168 |     schema_name = data.get("schema", "public")
169 |     
170 |     if not postgres_uri or not database_name or not table_name:
171 |         raise HTTPException(status_code=400, detail="Missing PostgreSQL URI, database, or table name")
172 |     
173 |     try:
174 |         new_uri = modify_uri_database(postgres_uri, database_name)
175 |         conn = connect_to_postgres(new_uri)
176 |         cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
177 |         
178 |         full_table_name = f'"{schema_name}"."{table_name}"'
179 |         
180 |         cursor.execute("""
181 |             SELECT EXISTS (
182 |                 SELECT FROM information_schema.tables 
183 |                 WHERE table_schema = %s AND table_name = %s
184 |             )
185 |         """, (schema_name, table_name))
186 |         
187 |         if not cursor.fetchone()[0]:
188 |             raise HTTPException(status_code=404, detail=f"Table {schema_name}.{table_name} not found")
189 |         
190 |         query = f"SELECT * FROM {full_table_name} LIMIT 5"
191 |         cursor.execute(query)
192 |         
193 |         columns = [desc[0] for desc in cursor.description]
194 |         records = []
195 |         
196 |         for row in cursor.fetchall():
197 |             record = {}
198 |             for i, col in enumerate(columns):
199 |                 if isinstance(row[i], (bytes, memoryview)):
200 |                     record[col] = "binary data"
201 |                 else:
202 |                     record[col] = row[i]
203 |             records.append(record)
204 |         
205 |         cursor.close()
206 |         conn.close()
207 |         
208 |         return {"records": records}
209 |     
210 |     except Exception as e:
211 |         raise HTTPException(status_code=500, detail=str(e))
212 | 
--------------------------------------------------------------------------------
/secrets-ninja-proxy/services/rabbitmq.py:
--------------------------------------------------------------------------------
 1 | from fastapi import APIRouter, Request, HTTPException
 2 | import pika
 3 | import requests
 4 | from requests.auth import HTTPBasicAuth
 5 | 
 6 | router = APIRouter()
 7 | 
 8 | def parse_connection_string(connection_string):
 9 |     if not connection_string.startswith(("amqp://", "amqps://")):
10 |         raise ValueError("Invalid AMQP/AMQPS connection string")
11 |     scheme_removed = connection_string.split("://", 1)[1]
12 |     creds, host_port = scheme_removed.split("@")
13 |     username, password = creds.split(":")
14 |     if ":" in host_port:
15 |         host, port = host_port.split(":")
16 |     else:
17 |         host = host_port
18 |         port = "5671" if connection_string.startswith("amqps://") else "5672"
19 |     return username, password, host, port
20 | 
21 | @router.post("/get_queues")
22 | async def get_queues(request: Request):
23 |     data = await request.json()
24 |     connection_string = data.get("connection_string")
25 |     if not connection_string:
26 |         raise HTTPException(status_code=400, detail="Missing connection string")
27 |     try:
28 |         username, password, host, port = parse_connection_string(connection_string)
29 |         protocol = "https" if connection_string.startswith("amqps://") else "http"
30 |         url = f"{protocol}://{host}:15672/api/queues"
31 |         response = requests.get(url, auth=HTTPBasicAuth(username, password), timeout=15, verify=False if protocol=="https" else True)
32 |         if response.status_code == 200:
33 |             queues = response.json()
34 |             result = []
35 |             for q in queues:
36 |                 result.append({
37 |                     "name": q["name"],
38 |                     "messages": q["messages"],
39 |                     "consumers": q["consumers"]
40 |                 })
41 |             return {"queues": result}
42 |         else:
43 |             raise HTTPException(status_code=500, detail=f"Failed to get queues, HTTP {response.status_code}")
44 |     except Exception as e:
45 |         raise HTTPException(status_code=500, detail=str(e))
46 | 
47 | @router.post("/get_queue_data")
48 | async def get_queue_data(request: Request):
49 |     data = await request.json()
50 |     connection_string = data.get("connection_string")
51 |     queue_name = data.get("queue_name")
52 |     if not connection_string or not queue_name:
53 |         raise HTTPException(status_code=400, detail="Missing connection string or queue name")
54 |     try:
55 |         params = pika.URLParameters(connection_string)
56 |         connection = pika.BlockingConnection(params)
57 |         channel = connection.channel()
58 | 
59 |         messages = []
60 |         for _ in range(5):
61 |             method_frame, header_frame, body = channel.basic_get(queue=queue_name, auto_ack=True)
62 |             if method_frame:
63 |                 messages.append(body.decode())
64 |             else:
65 |                 break
66 | 
67 |         connection.close()
68 |         return {"messages": messages}
69 |     except Exception as e:
70 |         raise HTTPException(status_code=500, detail=str(e))
71 | 
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
 1 | #root {
 2 |   max-width: 1280px;
 3 |   margin: 0 auto;
 4 |   padding: 2rem;
 5 |   text-align: center;
 6 | }
 7 | 
 8 | .logo {
 9 |   height: 6em;
10 |   padding: 1.5em;
11 |   will-change: filter;
12 |   transition: filter 300ms;
13 | }
14 | .logo:hover {
15 |   filter: drop-shadow(0 0 2em #646cffaa);
16 | }
17 | .logo.react:hover {
18 |   filter: drop-shadow(0 0 2em #61dafbaa);
19 | }
20 | 
21 | @keyframes logo-spin {
22 |   from {
23 |     transform: rotate(0deg);
24 |   }
25 |   to {
26 |     transform: rotate(360deg);
27 |   }
28 | }
29 | 
30 | @media (prefers-reduced-motion: no-preference) {
31 |   a:nth-of-type(2) .logo {
32 |     animation: logo-spin infinite 20s linear;
33 |   }
34 | }
35 | 
36 | .card {
37 |   padding: 2em;
38 | }
39 | 
40 | .read-the-docs {
41 |   color: #888;
42 | }
43 | 
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
  1 | import { useState, useEffect } from 'react';
  2 | import FlowbiteNavbar from './components/navbar';
  3 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
  4 | import SB from './components/sidebar';
  5 | import { Flowbite } from 'flowbite-react';
  6 | import UniversalComponent from './modules/universal';
  7 | import Dashboard from './components/dashboard';
  8 | import servicesConfig from './data/detectors.json';
  9 | 
 10 | // Sort servicesConfig by keys alphabetically
 11 | const sortedServicesConfig = Object.keys(servicesConfig)
 12 |   .sort()
 13 |   .reduce((result, key) => {
 14 |     result[key] = servicesConfig[key];
 15 |     return result;
 16 |   }, {});
 17 | 
 18 | // Custom hook to listen to window resize events
 19 | const useWindowSize = () => {
 20 |   const [size, setSize] = useState([window.innerWidth, window.innerHeight]);
 21 |   useEffect(() => {
 22 |     const handleResize = () => {
 23 |       setSize([window.innerWidth, window.innerHeight]);
 24 |     };
 25 |     window.addEventListener('resize', handleResize);
 26 |     return () => window.removeEventListener('resize', handleResize);
 27 |   }, []);
 28 |   return size;
 29 | };
 30 | 
 31 | export default function MyPage() {
 32 |   const [sidebarVisible, setSidebarVisible] = useState(true);
 33 |   const [width] = useWindowSize();
 34 | 
 35 |   const toggleSidebar = () => {
 36 |     setSidebarVisible(!sidebarVisible);
 37 |   };
 38 | 
 39 |   const isLargeScreen = width >= 1024;
 40 | 
 41 |   // Adjusted styles to ensure footer is always visible and not overlaid
 42 |   const containerStyle = `flex flex-col h-screen`;
 43 |   const contentContainerStyle = isLargeScreen
 44 |     ? 'flex flex-1 overflow-hidden'
 45 |     : 'flex flex-1 overflow-hidden';
 46 |   const sidebarStyle = isLargeScreen
 47 |     ? { maxHeight: 'calc(100vh - 60px)', overflowY: 'auto' } // Adjust for navbar height
 48 |     : {
 49 |         zIndex: 30,
 50 |         position: 'fixed',
 51 |         width: '100%',
 52 |         height: 'calc(100vh - 60px)',
 53 |         overflowY: 'auto',
 54 |       };
 55 |   const contentStyle = isLargeScreen
 56 |     ? 'flex-1 bg-gray-100 dark:bg-gray-700 overflow-auto'
 57 |     : `flex-1 bg-gray-100 dark:bg-gray-700 overflow-auto ${sidebarVisible ? 'sidebar-overlay' : ''}`;
 58 | 
 59 |   return (
 60 |     
 61 |       
 62 |         
 63 |           
 64 |              
 65 |           
 66 |           
 {
 69 |               if (!isLargeScreen && sidebarVisible) {
 70 |                 toggleSidebar();
 71 |               }
 72 |             }}
 73 |           >
 74 |             {sidebarVisible && (
 75 |               
 76 |                  
 81 |               
 82 |             )}
 83 |             
 84 |               
 85 |                  }
 88 |                 />
 89 |                 {Object.keys(sortedServicesConfig).map((service) => (
 90 |                    
 98 |                     }
 99 |                   />
100 |                 ))}
101 |                
102 |             
103 |           
104 |         
 
105 |        
106 |      
107 |   );
108 | }
109 | 
--------------------------------------------------------------------------------
/src/assets/logo-t.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NikhilPanwar/secrets-ninja/9db8616d4024cfe7329b439818769f94b8cde70a/src/assets/logo-t.png
--------------------------------------------------------------------------------
/src/assets/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NikhilPanwar/secrets-ninja/9db8616d4024cfe7329b439818769f94b8cde70a/src/assets/logo.jpg
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |  
--------------------------------------------------------------------------------
/src/components/copy_button.jsx:
--------------------------------------------------------------------------------
 1 | import React, { useState } from 'react';
 2 | import { Button } from 'flowbite-react';
 3 | import { FaRegClipboard, FaCheck } from 'react-icons/fa';
 4 | 
 5 | const CopyButton = ({ textToCopy }) => {
 6 |   const [buttonText, setButtonText] = useState('Copy');
 7 |   const [isCopied, setIsCopied] = useState(false);
 8 | 
 9 |   const copyToClipboard = () => {
10 |     navigator.clipboard
11 |       .writeText(textToCopy)
12 |       .then(() => {
13 |         setButtonText('Copied');
14 |         setIsCopied(true);
15 |         setTimeout(() => {
16 |           setButtonText('Copy');
17 |           setIsCopied(false);
18 |         }, 1000);
19 |       })
20 |       .catch((err) => {
21 |         console.error('Could not copy text: ', err);
22 |       });
23 |   };
24 | 
25 |   return (
26 |     
27 |       {isCopied ? (
28 |          
29 |       ) : (
30 |          
31 |       )}
32 |       {buttonText}
33 |      
34 |   );
35 | };
36 | 
37 | export default CopyButton;
38 | 
--------------------------------------------------------------------------------
/src/components/dashboard.jsx:
--------------------------------------------------------------------------------
 1 | import React from 'react';
 2 | import MainPageTable from './table';
 3 | import { Alert, Button } from 'flowbite-react';
 4 | import { ImNewTab } from 'react-icons/im';
 5 | import { FaGithub } from 'react-icons/fa';
 6 | import { FaXTwitter } from 'react-icons/fa6';
 7 | 
 8 | export default function Dashboard({ servicesConfig }) {
 9 |   return (
10 |     
11 |       
12 |         
13 |           
14 |             
15 |               Found API Keys, Credentials while Pentesting!
16 |              {' '}
17 |             Test them using Secrets Ninja ...
18 |            
19 |           
20 |             Note:  Secrets Ninja is an{' '}
21 |             
27 |               open source
28 |              
29 |               project for validating
30 |             secrets from the frontend using JS in your browser.
31 |            
32 |           
33 |             By: {' '}
34 |             
40 |               @SecretsN1nja{' '}
41 |              
42 |            
43 |           
44 |             Links: {' '}
45 |             
51 |                
52 |              
53 |             
59 |                
60 |              
61 |            
62 |          
63 |        
64 |       
65 |         
66 |           Supported Keys
67 |          
68 |         
69 |            
70 |         
71 |       
72 |     
 
73 |   );
74 | }
75 | 
--------------------------------------------------------------------------------
/src/components/footer.jsx:
--------------------------------------------------------------------------------
 1 | 'use client';
 2 | 
 3 | import { Footer } from 'flowbite-react';
 4 | import {
 5 |   BsDribbble,
 6 |   BsFacebook,
 7 |   BsGithub,
 8 |   BsInstagram,
 9 |   BsTwitter,
10 | } from 'react-icons/bs';
11 | import logo from '../assets/logo-t.png';
12 | 
13 | function FT() {
14 |   return (
15 |     
16 |       
17 |         
18 |           
19 |              
25 |           
26 |           
27 |             
28 |                
29 |               
30 |                 Team 
31 |                 Mission 
32 |                
33 |             
34 |             
35 |                
36 |               
37 |                 Github 
38 |                 Twitter 
39 |                
40 |             
41 |             
42 |                
43 |               
44 |                 Privacy Policy 
45 |                 Terms & Conditions 
46 |                
47 |             
48 |           
49 |         
50 |         
51 |         
52 |           
53 |           
54 |              
55 |              
56 |           
57 |         
58 |       
 
59 |      
60 |   );
61 | }
62 | 
63 | export default FT;
64 | 
--------------------------------------------------------------------------------
/src/components/navbar.jsx:
--------------------------------------------------------------------------------
 1 | import React, { useState } from 'react'; // Import useState
 2 | import { Navbar } from 'flowbite-react';
 3 | import { DarkThemeToggle } from 'flowbite-react';
 4 | import logo from '../assets/logo-t.png';
 5 | import Hamburger from 'hamburger-react';
 6 | 
 7 | function FlowbiteNavbar({ toggleSidebar }) {
 8 |   const [isOpen, setIsOpen] = useState(true); // State to manage sidebar open/close
 9 | 
10 |   // Function to toggle sidebar and hamburger state
11 |   const handleToggle = () => {
12 |     // setIsOpen(!isOpen); // Toggle the state
13 |     toggleSidebar(); // Call the prop function to actually toggle the sidebar
14 |   };
15 | 
16 |   const toggleStyle = {
17 |     fontSize: '0.8rem',
18 |     padding: '10px',
19 |   };
20 | 
21 |   return (
22 |     
23 |       
24 |         
34 |         
35 |            
36 |           
37 |             Secrets Ninja
38 |            
39 |          
40 |       
 
41 |       {/*   */}
42 |       {/*  */}
43 |       {/* 
44 |           Keys Checker
45 |           */}
46 |       {/* Find Your Secrets  */}
47 |       
48 |          
49 |       
50 |       {/*   */}
51 |      
52 |   );
53 | }
54 | 
55 | export default FlowbiteNavbar;
56 | 
--------------------------------------------------------------------------------
/src/components/pages/secrets_checker.jsx:
--------------------------------------------------------------------------------
1 | export {};
2 | 
--------------------------------------------------------------------------------
/src/components/request_window.jsx:
--------------------------------------------------------------------------------
 1 | import React from 'react';
 2 | import { Card } from 'flowbite-react';
 3 | import CopyButton from './copy_button';
 4 | import '../css/json_theme.css';
 5 | import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
 6 | import bash from 'react-syntax-highlighter/dist/esm/languages/hljs/bash';
 7 | import { github } from 'react-syntax-highlighter/dist/esm/styles/hljs';
 8 | 
 9 | SyntaxHighlighter.registerLanguage('bash', bash);
10 | 
11 | const customStyle = {
12 |   ...github,
13 |   'hljs': {
14 |     ...github['hljs'],
15 |     background: 'transparent',
16 |     color: '#333'
17 |   },
18 |   'hljs-string': {
19 |     ...github['hljs-string'],
20 |     color: '#ff5e5e'
21 |   },
22 |   'hljs-literal': {
23 |     ...github['hljs-literal'],
24 |     color: '#ff5e5e'
25 |   },
26 |   'hljs-number': {
27 |     ...github['hljs-number'],
28 |     color: '#ff5e5e'
29 |   },
30 |   'hljs-built_in': {
31 |     ...github['hljs-built_in'],
32 |     color: '#ff5e5e'
33 |   }
34 | };
35 | 
36 | function RequestWindow({ curl = '' }) {
37 |   return (
38 |     
39 |       
40 |         
Request 
41 |          
42 |       
43 |       
44 |         
53 |           {curl}
54 |          
55 |       
56 |      
57 |   );
58 | }
59 | 
60 | export default RequestWindow;
61 | 
--------------------------------------------------------------------------------
/src/components/requests.jsx:
--------------------------------------------------------------------------------
  1 | /* eslint-disable no-case-declarations */
  2 | async function makeUniversalRequest(
  3 |   isProxyEnabled,
  4 |   serviceType,
  5 |   inputData,
  6 |   endpointURL,
  7 |   requestMethod
  8 | ) {
  9 |   let response, data;
 10 |   const proxyURL = import.meta.env.VITE_SECRETS_NINJA_PROXY_ENDPOINT;
 11 |   endpointURL = isProxyEnabled
 12 |     ? proxyURL + `/fetch/` + endpointURL
 13 |     : endpointURL;
 14 | 
 15 |   switch (serviceType) {
 16 |     case 'Stripe':
 17 |       response = await fetch(endpointURL, {
 18 |         method: 'GET',
 19 |         headers: {
 20 |           Authorization: `Bearer ${inputData.api_key}`,
 21 |         },
 22 |       });
 23 |       break;
 24 |     case 'Paypal':
 25 |       let accessToken;
 26 |       const credentials = `${inputData.client_id}:${inputData.client_secret}`;
 27 |       const encodedCredentials = btoa(credentials);
 28 |       let tokenResponse = await fetch(
 29 |         'https://api.paypal.com/v1/oauth2/token',
 30 |         {
 31 |           method: 'POST',
 32 |           headers: {
 33 |             Accept: 'application/json',
 34 |             'Accept-Language': 'en_US',
 35 |             Authorization: `Basic ${encodedCredentials}`,
 36 |           },
 37 |           body: new URLSearchParams({ grant_type: 'client_credentials' }),
 38 |         }
 39 |       );
 40 |       if (endpointURL !== 'https://api.paypal.com/v1/oauth2/token') {
 41 |         const tokenData = await tokenResponse.json();
 42 |         accessToken = tokenData.access_token;
 43 |         response = await fetch(endpointURL, {
 44 |           method: 'GET',
 45 |           headers: {
 46 |             Authorization: `Bearer ${accessToken}`,
 47 |             'Content-Type': 'application/json',
 48 |           },
 49 |         });
 50 |       } else {
 51 |         response = tokenResponse;
 52 |       }
 53 |       break;
 54 |     case 'OpenAI':
 55 |       response = await fetch(endpointURL, {
 56 |         method: requestMethod,
 57 |         headers: {
 58 |           Authorization: `Bearer ${inputData.api_key}`,
 59 |         },
 60 |       });
 61 |       break;
 62 |     case 'Paystack':
 63 |       response = await fetch(endpointURL, {
 64 |         method: requestMethod,
 65 |         headers: {
 66 |           Authorization: `Bearer ${inputData.api_key}`,
 67 |         },
 68 |       });
 69 |       break;
 70 |     case 'Github':
 71 |       response = await fetch(
 72 |         endpointURL
 73 |           .replace('', inputData.org_name)
 74 |           .replace('', inputData.package_type)
 75 |           .replace('', inputData.query),
 76 |         {
 77 |           method: requestMethod,
 78 |           headers: {
 79 |             Authorization: `token ${inputData.access_token}`,
 80 |           },
 81 |         }
 82 |       );
 83 |       break;
 84 |     case 'LaunchDarkly':
 85 |       response = await fetch(endpointURL, {
 86 |         method: requestMethod,
 87 |         headers: {
 88 |           Authorization: ` ${inputData.api_key}`,
 89 |         },
 90 |       });
 91 |       break;
 92 |     case 'Omnisend':
 93 |       response = await fetch(endpointURL, {
 94 |         method: requestMethod,
 95 |         headers: {
 96 |           'X-API-KEY': `${inputData.api_key}`,
 97 |         },
 98 |       });
 99 |       break;
100 |     case 'Telegram':
101 |       response = await fetch(
102 |         endpointURL.replace('', inputData.bot_token),
103 |         {
104 |           method: requestMethod,
105 |         }
106 |       );
107 |       break;
108 |     case 'Clearbit':
109 |       response = await fetch(endpointURL + inputData.email, {
110 |         method: requestMethod,
111 |         headers: {
112 |           Authorization: 'Basic ' + btoa(inputData.api_key + ':'),
113 |         },
114 |       });
115 |       break;
116 |     case 'SendInBlue':
117 |       response = await fetch(endpointURL, {
118 |         method: requestMethod,
119 |         headers: {
120 |           'api-key': `${inputData.api_key}`,
121 |         },
122 |       });
123 |       break;
124 |     case 'Twitter':
125 |       const isTokenEndpoint = endpointURL.includes('oauth2/token');
126 |       const options = isTokenEndpoint
127 |         ? {
128 |           method: 'POST',
129 |           headers: { 'Content-Type': 'application/json' },
130 |           body: JSON.stringify({
131 |             proxied_data: {
132 |               headers: {
133 |                 Authorization:
134 |                   'Basic ' + btoa(`${inputData.api_key}:${inputData.api_secret_key}`),
135 |                 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
136 |               },
137 |             },
138 |             body: 'grant_type=client_credentials',
139 |           }),
140 |         }
141 |         : {
142 |           method: requestMethod,
143 |           headers: {
144 |             Authorization: 'Bearer ' + inputData.access_token,
145 |           },
146 |         };
147 |       response = await fetch(endpointURL, options);
148 |       break;
149 |     case 'RechargePayments':
150 |       response = await fetch(endpointURL, {
151 |         method: requestMethod,
152 |         headers: {
153 |           'X-Recharge-Access-Token': `${inputData.api_key}`,
154 |         },
155 |       });
156 |       break;
157 |     case 'MailerLite':
158 |       response = await fetch(endpointURL, {
159 |         method: requestMethod,
160 |         headers: {
161 |           'X-MailerLite-ApiKey': `${inputData.api_key}`,
162 |         },
163 |       });
164 |       break;
165 |     case 'Trello':
166 |       response = await fetch(
167 |         endpointURL
168 |           .replace('', inputData.key)
169 |           .replace('', inputData.token),
170 |         {
171 |           method: requestMethod,
172 |         }
173 |       );
174 |       break;
175 |     case 'RazorPay':
176 |       response = await fetch(endpointURL, {
177 |         method: requestMethod,
178 |         headers: {
179 |           Authorization: `Basic ${btoa(inputData.key_id + ':' + inputData.key_secret)}`,
180 |         },
181 |       });
182 |       break;
183 |     case 'Twilio':
184 |       response = await fetch(
185 |         endpointURL.replace('', inputData.account_sid),
186 |         {
187 |           method: requestMethod,
188 |           headers: {
189 |             Authorization: `Basic ${btoa(inputData.account_sid + ':' + inputData.auth_token)}`,
190 |           },
191 |         }
192 |       );
193 |       break;
194 |     case 'NpmToken':
195 |       response = await fetch(endpointURL.replace('', inputData.org).replace('', inputData.user), {
196 |         method: requestMethod,
197 |         headers: {
198 |           Authorization: `Bearer ${inputData.token}`,
199 |         },
200 |       });
201 |       break;
202 |     case 'Mailgun':
203 |       response = await fetch(
204 |         endpointURL.replace('', inputData.domain),
205 |         {
206 |           method: requestMethod,
207 |           headers: {
208 |             Authorization: `Basic ${btoa('api:' + inputData.api_key)}`,
209 |           },
210 |         }
211 |       );
212 |       break;
213 |     case 'Klaviyo':
214 |       response = await fetch(endpointURL, {
215 |         method: requestMethod,
216 |         headers: {
217 |           Revision: '2023-02-22',
218 |           Authorization: `Klaviyo-API-Key ${inputData.api_key}`,
219 |         },
220 |       });
221 |       break;
222 |     case 'DigitalOcean':
223 |       response = await fetch(endpointURL, {
224 |         method: requestMethod,
225 |         headers: {
226 |           Authorization: `Bearer ${inputData.api_key}`,
227 |         },
228 |       });
229 |       break;
230 |     case 'Honeycomb':
231 |       response = await fetch(endpointURL, {
232 |         method: requestMethod,
233 |         headers: {
234 |           'X-Honeycomb-Team': `${inputData.api_key}`,
235 |         },
236 |       });
237 |       break;
238 |     case 'Eventbrite':
239 |       response = await fetch(endpointURL + `?token=${inputData.token}`, {
240 |         method: requestMethod,
241 |       });
242 |       break;
243 |     case 'SendGrid':
244 |       response = await fetch(endpointURL, {
245 |         method: requestMethod,
246 |         headers: {
247 |           Authorization: `Bearer ${inputData.api_key}`,
248 |         },
249 |       });
250 |       break;
251 |     case 'MailChimp':
252 |       response = await fetch(endpointURL.replace('', inputData.dc), {
253 |         method: requestMethod,
254 |         headers: {
255 |           Authorization: `Basic ${btoa('anystring:' + inputData.api_key)}`,
256 |         },
257 |       });
258 |       break;
259 |     case 'Postmark':
260 |       response = await fetch(endpointURL, {
261 |         method: requestMethod,
262 |         headers: {
263 |           'Content-Type': 'application/json',
264 |           'X-Postmark-Server-Token': `${inputData.server_token}`,
265 |         },
266 |       });
267 |       break;
268 |     case 'Telnyx':
269 |       response = await fetch(endpointURL, {
270 |         method: requestMethod,
271 |         headers: {
272 |           Authorization: `Bearer ${inputData.api_key}`,
273 |         },
274 |       });
275 |       break;
276 |     case 'Pipedrive':
277 |       response = await fetch(
278 |         endpointURL + `?api_token=${inputData.api_token}`,
279 |         {
280 |           method: requestMethod,
281 |         }
282 |       );
283 |       break;
284 |     case 'Vercel':
285 |       response = await fetch(endpointURL, {
286 |         method: requestMethod,
287 |         headers: {
288 |           Authorization: `Bearer ${inputData.api_token}`,
289 |         },
290 |       });
291 |       break;
292 |     case 'Bitly':
293 |       response = await fetch(endpointURL, {
294 |         method: requestMethod,
295 |         headers: {
296 |           Authorization: `Bearer ${inputData.api_token}`,
297 |         },
298 |       });
299 |       break;
300 |     case 'Algolia':
301 |       response = await fetch(
302 |         endpointURL.replace('', inputData.app_id),
303 |         {
304 |           method: requestMethod,
305 |           headers: {
306 |             'X-Algolia-API-Key': `${inputData.api_key}`,
307 |             'X-Algolia-Application-Id': `${inputData.app_id}`,
308 |           },
309 |         }
310 |       );
311 |       break;
312 |     case 'Posthog':
313 |       response = await fetch(
314 |         endpointURL.replace('', inputData.api_key),
315 |         {
316 |           method: requestMethod,
317 |         }
318 |       );
319 |       if (response.status === 401) {
320 |         response = await fetch(
321 |           endpointURL
322 |             .replace('', inputData.api_key)
323 |             .replace('https://app.', 'https://eu.'),
324 |           {
325 |             method: requestMethod,
326 |           }
327 |         );
328 |       }
329 |       break;
330 |     case 'Opsgenie':
331 |       response = await fetch(endpointURL, {
332 |         method: requestMethod,
333 |         headers: {
334 |           Authorization: `GenieKey ${inputData.api_key}`,
335 |         },
336 |       });
337 |       break;
338 |     case 'Helpscout':
339 |       response = await fetch(endpointURL, {
340 |         method: requestMethod,
341 |         headers: {
342 |           Authorization: 'Basic ' + btoa(inputData.api_key + ':'),
343 |         },
344 |       });
345 |       break;
346 |     case 'Typeform':
347 |       response = await fetch(endpointURL, {
348 |         method: requestMethod,
349 |         headers: {
350 |           Authorization: `Bearer ${inputData.api_key}`,
351 |         },
352 |       });
353 |       break;
354 |     case 'GetResponse':
355 |       response = await fetch(endpointURL, {
356 |         method: requestMethod,
357 |         headers: {
358 |           'X-Auth-Token': `api-key ${inputData.api_key}`,
359 |         },
360 |       });
361 |       break;
362 |     case 'YouSign':
363 |       response = await fetch(endpointURL, {
364 |         method: requestMethod,
365 |         headers: {
366 |           Authorization: `Bearer ${inputData.api_key}`,
367 |         },
368 |       });
369 |       break;
370 |     case 'Notion':
371 |       response = await fetch(endpointURL, {
372 |         method: requestMethod,
373 |         headers: {
374 |           Authorization: `Bearer ${inputData.api_key}`,
375 |           'Notion-Version': '2022-06-28',
376 |         },
377 |       });
378 |       break;
379 |     case 'MadKudu':
380 |       response = await fetch(endpointURL, {
381 |         method: requestMethod,
382 |         headers: {
383 |           Authorization: `Basic ${btoa(inputData.api_key + ':')}`,
384 |         },
385 |       });
386 |       break;
387 |     case 'Autopilot':
388 |       response = await fetch(endpointURL, {
389 |         method: requestMethod,
390 |         headers: {
391 |           autopilotapikey: `${inputData.api_key}`,
392 |         },
393 |       });
394 |       break;
395 |     case 'Slack':
396 |       response = await fetch(endpointURL.replace('', inputData.channel_id), {
397 |         method: requestMethod,
398 |         headers: {
399 |           Authorization: `Bearer ${inputData.api_token}`,
400 |         },
401 |       });
402 |       break;
403 |     case 'Gitlab':
404 |       response = await fetch(
405 |         endpointURL
406 |           .replace('', inputData.project_id)
407 |           .replace('', inputData.user_id),
408 |         {
409 |           method: requestMethod,
410 |           headers: {
411 |             Authorization: `Bearer ${inputData.access_token}`,
412 |           },
413 |         }
414 |       );
415 |       break;
416 |     case 'BitBucket':
417 |       response = await fetch(endpointURL, {
418 |         method: requestMethod,
419 |         headers: {
420 |           Authorization: `Basic ${btoa(inputData.username + ':' + inputData.password)}`,
421 |         },
422 |       });
423 |       break;
424 |     case 'HuggingFace':
425 |       response = await fetch(endpointURL.replace('', inputData.org), {
426 |         method: requestMethod,
427 |         headers: {
428 |           authorization: `Bearer ${inputData.api_token}`,
429 |         },
430 |       });
431 |       break;
432 |     case 'Shodan':
433 |       response = await fetch(
434 |         endpointURL.replace('', inputData.api_key),
435 |         {
436 |           method: requestMethod,
437 |         }
438 |       );
439 |       break;
440 |     case 'Postman':
441 |       response = await fetch(endpointURL, {
442 |         method: requestMethod,
443 |         headers: {
444 |           'X-Api-Key': `${inputData.api_key}`,
445 |         },
446 |       });
447 |       break;
448 |     case 'Terraform':
449 |       response = await fetch(endpointURL, {
450 |         method: requestMethod,
451 |         headers: {
452 |           Authorization: `Bearer ${inputData.api_token}`,
453 |         },
454 |       });
455 |       break;
456 |     case 'Doppler':
457 |       response = await fetch(endpointURL, {
458 |         method: requestMethod,
459 |         headers: {
460 |           Authorization: `Bearer ${inputData.api_key}`,
461 |         },
462 |       });
463 |       break;
464 |     case 'Shopify':
465 |       response = await fetch(
466 |         endpointURL.replace(
467 |           encodeURIComponent(''),
468 |           inputData.store_domain
469 |         ),
470 |         {
471 |           method: requestMethod,
472 |           headers: {
473 |             'X-Shopify-Access-Token': `${inputData.api_key}`,
474 |           },
475 |         }
476 |       );
477 |       break;
478 |     case 'Jfrog':
479 |       response = await fetch(
480 |         endpointURL.replace(encodeURIComponent(''), inputData.domain),
481 |         {
482 |           method: requestMethod,
483 |           headers: {
484 |             Authorization: `Basic ${btoa(`${inputData.username}:${inputData.password}`)}`,
485 |             'Content-Type': 'application/json',
486 |           },
487 |         }
488 |       );
489 |       break;
490 |     case 'Buildkite':
491 |       response = await fetch(endpointURL, {
492 |         method: requestMethod,
493 |         headers: {
494 |           Authorization: `Bearer ${inputData.api_key}`,
495 |         },
496 |       });
497 |       break;
498 |     case 'Pulumi':
499 |       response = await fetch(endpointURL, {
500 |         method: requestMethod,
501 |         headers: {
502 |           Authorization: `token ${inputData.api_key}`,
503 |         },
504 |       });
505 |       break;
506 |     case 'Snyk':
507 |       response = await fetch(endpointURL, {
508 |         method: requestMethod,
509 |         headers: {
510 |           Authorization: `token ${inputData.api_key}`,
511 |         },
512 |       });
513 |       break;
514 |     case 'EvolutionAPI':
515 |       response = await fetch(
516 |         endpointURL.replace('', inputData.instance_url),
517 |         {
518 |           method: requestMethod,
519 |           headers: {
520 |             apikey: `${inputData.api_key}`,
521 |           },
522 |         }
523 |       );
524 |       break;
525 |     case 'PushBullet':
526 |       response = await fetch(endpointURL, {
527 |         method: requestMethod,
528 |         headers: {
529 |           'Access-Token': `${inputData.api_key}`,
530 |         },
531 |       });
532 |       break;
533 |     case 'SquareAccessToken':
534 |       response = await fetch(endpointURL, {
535 |         method: requestMethod,
536 |         headers: {
537 |           Authorization: `Bearer ${inputData.access_token}`,
538 |         },
539 |       });
540 |       break;
541 |     case 'Sentry':
542 |       response = await fetch(endpointURL, {
543 |         method: requestMethod,
544 |         headers: {
545 |           Authorization: `Bearer ${inputData.auth_token}`,
546 |         },
547 |       });
548 |       break;
549 |     case 'Bitbucket':
550 |       response = await fetch(endpointURL.replace('', inputData.org), {
551 |         method: requestMethod,
552 |         headers: {
553 |           Authorization: `Basic ${btoa(inputData.username + ':' + inputData.password)}`,
554 |         },
555 |       });
556 |       break;
557 |     case 'Jira':
558 |       response = await fetch(endpointURL.replace(encodeURIComponent(''), inputData.app_domain), {
559 |         method: requestMethod,
560 |         headers: {
561 |           Authorization: `Basic ${btoa(inputData.email + ':' + inputData.api_token)}`,
562 |           Accept: 'application/json',
563 |         },
564 |       });
565 |       break;
566 |     case 'Pandadoc':
567 |       response = await fetch(endpointURL, {
568 |         method: requestMethod,
569 |         headers: {
570 |           Authorization: `API-Key ${inputData.api_key}`,
571 |         },
572 |       });
573 |       break;
574 |     case 'HubSpot':
575 |       response = await fetch(endpointURL, {
576 |         method: requestMethod,
577 |         headers: {
578 |           Authorization: `Bearer ${inputData.access_token}`,
579 |           'Content-Type': 'application/json'
580 |         },
581 |       });
582 |       break;
583 |     case 'AWS':
584 |       response = await fetch(endpointURL.replace('', proxyURL), {
585 |         method: requestMethod,
586 |         headers: {
587 |           'Content-Type': 'application/json',
588 |         },
589 |         body: JSON.stringify({
590 |           aws_access_key: inputData.access_key,
591 |           aws_secret_key: inputData.secrets_access_key,
592 |           region: inputData.region,
593 |         }),
594 |       });
595 |       break;
596 |     case 'MongoDB':
597 |       response = await fetch(endpointURL.replace('', proxyURL), {
598 |         method: requestMethod,
599 |         headers: {
600 |           'Content-Type': 'application/json',
601 |         },
602 |         body: JSON.stringify({
603 |           mongodb_uri: inputData.mongodb_uri,
604 |           database: inputData.database,
605 |           collection: inputData.collection,
606 |         }),
607 |       });
608 |       break;
609 |     case 'RabbitMQ':
610 |       response = await fetch(endpointURL.replace('', proxyURL), {
611 |         method: requestMethod,
612 |         headers: {
613 |           'Content-Type': 'application/json',
614 |         },
615 |         body: JSON.stringify({
616 |           connection_string: inputData.connection_string,
617 |           queue_name: inputData.queue_name,
618 |         }),
619 |       });
620 |       break;
621 |     case 'Postgres':
622 |       response = await fetch(endpointURL.replace('', proxyURL), {
623 |         method: requestMethod,
624 |         headers: {
625 |           'Content-Type': 'application/json',
626 |         },
627 |         body: JSON.stringify({
628 |           postgres_uri: inputData.connection_string,
629 |           database: inputData.database,
630 |           table: inputData.table,
631 |         }),
632 |       });
633 |       break;
634 |     case 'Zendesk':
635 |       response = await fetch(endpointURL.replace('', inputData.subdomain), {
636 |         method: requestMethod,
637 |         headers: {
638 |           Authorization: `Basic ${btoa(inputData.email + '/token:' + inputData.api_token)}`,
639 |         },
640 |       });
641 |       break;
642 |     case 'GCP':
643 |       response = await fetch(endpointURL.replace('', proxyURL), {
644 |         method: requestMethod,
645 |         headers: {
646 |           'Content-Type': 'application/json',
647 |         },
648 |         body: JSON.stringify({
649 |           gcp_creds: inputData.gcp_creds
650 |         }),
651 |       });
652 |       break;
653 |     case 'NVIDIA':
654 |       response = await fetch(endpointURL, {
655 |         method: 'POST',
656 |         body: JSON.stringify({
657 |           proxied_data: {
658 |             method: 'POST',
659 |             headers: {
660 |               "Content-Type": "application/x-www-form-urlencoded"
661 |             }
662 |           },
663 |           body: {
664 |             "credentials": inputData.api_key,
665 |           }
666 |         })
667 |       });
668 |       break;
669 |     case 'SonarCloud':
670 |       response = await fetch(endpointURL, {
671 |         method: requestMethod,
672 |         headers: {
673 |           Authorization: `Basic ${btoa(inputData.token + ':')}`,
674 |         },
675 |       });
676 |       break;
677 |     case 'Clerk':
678 |       response = await fetch(endpointURL, {
679 |         method: requestMethod,
680 |         headers: {
681 |           Authorization: `Bearer ${inputData.secret_key}`,
682 |         },
683 |       });
684 |       break;
685 |     case 'Okta':
686 |       response = await fetch(endpointURL.replace('', inputData.your_okta_domain), {
687 |         method: requestMethod,
688 |         headers: {
689 |           Authorization: `SSWS ${inputData.api_token}`
690 |         }
691 |       });
692 |       break;
693 |     case 'CircleCI':
694 |       response = await fetch(endpointURL, {
695 |         method: requestMethod,
696 |         headers: {
697 |           'Circle-Token': inputData.api_key,
698 |         },
699 |       });
700 |       break;
701 |     case 'WeightsAndBiases':
702 |   response = await fetch(endpointURL, {
703 |     method: 'POST',
704 |     body: JSON.stringify({
705 |       proxied_data: {
706 |         method: 'POST',
707 |         headers: {
708 |           Authorization: 'Basic ' + btoa(`api:${inputData.api_key}`),
709 |         },
710 |         body: JSON.stringify({
711 |           query: "query Viewer { viewer { id username email admin } }",
712 |         }),
713 |       },
714 |     }),
715 |   });
716 |   break;
717 |     default:
718 |       return { status: 400, data: { message: 'Unsupported service type' } };
719 |   }
720 | 
721 |   data = await response.json();
722 |   return { status: response.status, data };
723 | }
724 | 
725 | export default makeUniversalRequest;
726 | 
--------------------------------------------------------------------------------
/src/components/response_window.jsx:
--------------------------------------------------------------------------------
 1 | import { Card, Button } from 'flowbite-react';
 2 | import { Tabs } from 'flowbite-react';
 3 | import { BsTable } from "react-icons/bs";
 4 | import { VscJson } from "react-icons/vsc";
 5 | import CopyButton from './copy_button';
 6 | import RawJsonTab from './response_window/json_view';
 7 | import JsonGridView from './response_window/json_grid_view';
 8 | import '../css/json_theme.css';
 9 | 
10 | function OutputWindow({ status_code = 0, output_str = '{}' }) {
11 |   if (status_code === 0) return null;
12 | 
13 |   let parsedData;
14 |   try {
15 |     parsedData = JSON.parse(output_str);
16 |   } catch {
17 |     parsedData = output_str;
18 |   }
19 | 
20 |   return (
21 |     
22 |       
23 |         
Response 
24 |         
25 |            
26 |           
27 |             {status_code === 200 ? '🟢' : '🔴'} {status_code}
28 |            
29 |         
30 |       
 
31 | 
32 |       
33 |         
34 |            
35 |          
36 |         
37 |            
38 |          
39 |        
40 |      
41 |   );
42 | }
43 | 
44 | export default OutputWindow;
45 | 
--------------------------------------------------------------------------------
/src/components/response_window/json_grid_view.jsx:
--------------------------------------------------------------------------------
 1 | import { useState, useEffect } from 'react';
 2 | import JSONGrid from '@redheadphone/react-json-grid';
 3 | 
 4 | const lightTheme = {
 5 |   bgColor: 'transparent',
 6 |   keyColor: '#ff5e5e',
 7 |   stringColor: '#168f46',
 8 |   booleanColor: '#6699cc',
 9 |   numberColor: '#fdb082',
10 |   objectColor: '#9ca3af',
11 |   indexColor: '#9ca3af',
12 |   borderColor: '#d1d5db',
13 |   cellBorderColor: '#d1d5db',
14 |   tableHeaderBgColor: '#f9fafb',
15 |   tableIconColor: '#6b7280',
16 |   selectHighlightBgColor: '#f3f4f6',
17 | };
18 | 
19 | const darkTheme = {
20 |   bgColor: 'transparent',
21 |   keyColor: '#ff5e5e',
22 |   stringColor: '#168f46',
23 |   booleanColor: '#6699cc',
24 |   numberColor: '#fdb082',
25 |   objectColor: '#9ca3af',
26 |   indexColor: '#9ca3af',
27 |   borderColor: '#4b5563',
28 |   cellBorderColor: '#4b5563',
29 |   tableHeaderBgColor: '#1f2937',
30 |   tableIconColor: '#9ca3af',
31 |   selectHighlightBgColor: '#374151',
32 | };
33 | 
34 | export default function JsonGridView({ parsedData }) {
35 |   const [activeTheme, setActiveTheme] = useState(lightTheme);
36 | 
37 |   useEffect(() => {
38 |     const updateTheme = () => {
39 |       const isDarkMode = document.documentElement.classList.contains('dark');
40 |       setActiveTheme(isDarkMode ? darkTheme : lightTheme);
41 |     };
42 | 
43 |     updateTheme();
44 | 
45 |     const observer = new MutationObserver(updateTheme);
46 |     observer.observe(document.documentElement, {
47 |       attributes: true,
48 |       attributeFilter: ['class'],
49 |     });
50 | 
51 |     return () => {
52 |       observer.disconnect();
53 |     };
54 |   }, []);
55 | 
56 |   return (
57 |     
61 |        
66 |     
67 |   );
68 | }
--------------------------------------------------------------------------------
/src/components/response_window/json_view.jsx:
--------------------------------------------------------------------------------
 1 | import JSONPretty from 'react-json-pretty';
 2 | 
 3 | function RawJsonTab({ parsedData }) {
 4 |   return (
 5 |     
10 |   );
11 | }
12 | 
13 | export default RawJsonTab;
14 | 
--------------------------------------------------------------------------------
/src/components/sidebar.jsx:
--------------------------------------------------------------------------------
  1 | 'use client';
  2 | 
  3 | import { Sidebar } from 'flowbite-react';
  4 | import { LiaStripeS } from 'react-icons/lia';
  5 | import { RiOpenaiFill } from 'react-icons/ri';
  6 | import { HiOutlineRocketLaunch } from 'react-icons/hi2';
  7 | import { SlPaypal } from 'react-icons/sl';
  8 | import { FaGithub, FaShopify, FaTelegramPlane } from 'react-icons/fa';
  9 | import { CiCircleInfo } from 'react-icons/ci';
 10 | import { SiBrevo } from "react-icons/si";
 11 | import { FaTrello } from 'react-icons/fa';
 12 | import { IoMdArrowDroprightCircle } from 'react-icons/io';
 13 | import { SiRazorpay } from 'react-icons/si';
 14 | import { SiTwilio } from 'react-icons/si';
 15 | import { RiNpmjsLine } from 'react-icons/ri';
 16 | import { SiMailgun } from 'react-icons/si';
 17 | import { FaDigitalOcean } from 'react-icons/fa';
 18 | import { GiHoneycomb } from 'react-icons/gi';
 19 | import { SiEventbrite } from 'react-icons/si';
 20 | import { FaMailchimp } from 'react-icons/fa';
 21 | import { TbSquareLetterP } from 'react-icons/tb';
 22 | import { SiRavelry } from 'react-icons/si';
 23 | import { GrTextAlignFull } from 'react-icons/gr';
 24 | import { RiFlag2Line } from 'react-icons/ri';
 25 | import { TbCircleLetterP } from 'react-icons/tb';
 26 | import { IoLogoVercel } from 'react-icons/io5';
 27 | import { SiBitly } from 'react-icons/si';
 28 | import { SiAlgolia } from 'react-icons/si';
 29 | import { SiPosthog } from 'react-icons/si';
 30 | import { SiOpsgenie } from 'react-icons/si';
 31 | import { SiHelpscout } from 'react-icons/si';
 32 | import { SiTypeform } from 'react-icons/si';
 33 | import { SiNotion } from 'react-icons/si';
 34 | import { FaSlack } from 'react-icons/fa';
 35 | import { FaSquareGitlab } from 'react-icons/fa6';
 36 | import { SiPostman } from 'react-icons/si';
 37 | import { SiTerraform } from 'react-icons/si';
 38 | import { SiJfrog } from 'react-icons/si';
 39 | import { SiBuildkite } from 'react-icons/si';
 40 | import { SiPulumi } from 'react-icons/si';
 41 | import { SiSnyk } from 'react-icons/si';
 42 | import { CgSquare } from 'react-icons/cg';
 43 | import { SiSentry } from "react-icons/si";
 44 | import { FaBitbucket } from "react-icons/fa";
 45 | import { SiJira } from "react-icons/si";
 46 | import { SiHuggingface } from "react-icons/si";
 47 | import { SiSendgrid } from "react-icons/si";
 48 | import { FaHubspot } from "react-icons/fa";
 49 | import { FaAws } from "react-icons/fa";
 50 | import { SiMongodb } from "react-icons/si";
 51 | import { SiRabbitmq } from "react-icons/si";
 52 | import { BiLogoPostgresql } from "react-icons/bi";
 53 | import { SiZendesk } from "react-icons/si";
 54 | import { SiGooglecloud } from "react-icons/si";
 55 | import { BsNvidia } from "react-icons/bs";
 56 | import { SiSonar } from "react-icons/si";
 57 | import { SiClerk } from "react-icons/si";
 58 | import { FaXTwitter } from "react-icons/fa6";
 59 | import { SiOkta } from "react-icons/si";
 60 | import { SiCircleci } from "react-icons/si";
 61 | import { SiWeightsandbiases } from "react-icons/si";
 62 | import { useRef, useEffect } from 'react';
 63 | import { useMatch } from 'react-router-dom';
 64 | 
 65 | 
 66 | function SB({ visible, servicesConfig }) {
 67 |   // Accept visible as a prop
 68 |   if (!visible) return null; // Do not render if not visible
 69 | 
 70 |   let serviceIcons = {
 71 |     Stripe: LiaStripeS,
 72 |     Paypal: SlPaypal,
 73 |     OpenAI: RiOpenaiFill,
 74 |     LaunchDarkly: HiOutlineRocketLaunch,
 75 |     Github: FaGithub,
 76 |     Shopify: FaShopify,
 77 |     Telegram: FaTelegramPlane,
 78 |     SendInBlue: SiBrevo,
 79 |     Trello: FaTrello,
 80 |     RazorPay: SiRazorpay,
 81 |     Twilio: SiTwilio,
 82 |     NpmToken: RiNpmjsLine,
 83 |     Mailgun: SiMailgun,
 84 |     DigitalOcean: FaDigitalOcean,
 85 |     Honeycomb: GiHoneycomb,
 86 |     Eventbrite: SiEventbrite,
 87 |     MailChimp: FaMailchimp,
 88 |     Postmark: TbSquareLetterP,
 89 |     RechargePayments: SiRavelry,
 90 |     Paystack: GrTextAlignFull,
 91 |     Klaviyo: RiFlag2Line,
 92 |     Pipedrive: TbCircleLetterP,
 93 |     Vercel: IoLogoVercel,
 94 |     Bitly: SiBitly,
 95 |     Algolia: SiAlgolia,
 96 |     Posthog: SiPosthog,
 97 |     Opsgenie: SiOpsgenie,
 98 |     Helpscout: SiHelpscout,
 99 |     Typeform: SiTypeform,
100 |     Notion: SiNotion,
101 |     Slack: FaSlack,
102 |     Gitlab: FaSquareGitlab,
103 |     Postman: SiPostman,
104 |     Terraform: SiTerraform,
105 |     Jfrog: SiJfrog,
106 |     Buildkite: SiBuildkite,
107 |     Pulumi: SiPulumi,
108 |     Snyk: SiSnyk,
109 |     SquareAccessToken: CgSquare,
110 |     Sentry: SiSentry,
111 |     Bitbucket: FaBitbucket,
112 |     Jira: SiJira,
113 |     HuggingFace: SiHuggingface,
114 |     SendGrid: SiSendgrid,
115 |     HubSpot: FaHubspot,
116 |     AWS: FaAws,
117 |     MongoDB: SiMongodb,
118 |     RabbitMQ: SiRabbitmq,
119 |     Postgres: BiLogoPostgresql,
120 |     Zendesk: SiZendesk,
121 |     GCP: SiGooglecloud,
122 |     NVIDIA: BsNvidia,
123 |     SonarCloud: SiSonar,
124 |     Clerk: SiClerk,
125 |     Twitter: FaXTwitter,
126 |     Okta: SiOkta,
127 |     CircleCI: SiCircleci,
128 |     WeightsAndBiases: SiWeightsandbiases
129 |   };
130 | 
131 |   return (
132 |     
133 |       
134 |         
135 |           
136 |             Modules
137 |            
138 |           {/* 
139 |             About
140 |             */}
141 |           {Object.keys(servicesConfig).map((service) => {
142 |             const path = `/${service.toLowerCase()}`;
143 |             const isActive = useMatch(path);
144 |             const itemRef = useRef(null);
145 | 
146 |             useEffect(() => {
147 |               if (isActive && itemRef.current) {
148 |                 itemRef.current.scrollIntoView({ block: 'center' });
149 |               }
150 |             }, [isActive]);
151 | 
152 |             return (
153 |               
154 |                 
160 |                   {service}
161 |                  
162 |               
163 |             );
164 |           })}
165 | 
166 |          
167 |        
168 |      
169 |   );
170 | }
171 | 
172 | export default SB;
173 | 
--------------------------------------------------------------------------------
/src/components/table.jsx:
--------------------------------------------------------------------------------
 1 | 'use client';
 2 | 
 3 | import { Table } from 'flowbite-react';
 4 | 
 5 | function MainPageTable({ servicesConfig }) {
 6 |   // Helper function to capitalize first letter and replace underscores
 7 |   const formatEndpointName = (name) => {
 8 |     return name
 9 |       .split('_')
10 |       .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
11 |       .join(' ');
12 |   };
13 | 
14 |   // Function to transform service config into rows for rendering
15 |   const renderServiceRows = () => {
16 |     return Object.entries(servicesConfig).map(
17 |       ([serviceName, serviceDetails]) => (
18 |         
22 |           
23 |             
24 |               {serviceName}
25 |              
26 |            
27 |           
28 |             {Object.keys(serviceDetails.endpoints)
29 |               .map((endpointName) => formatEndpointName(endpointName))
30 |               .join(', ')}
31 |            
32 |           
33 |             {serviceDetails.api_documentation_page ? (
34 |               
40 |                 Docs
41 |                
42 |             ) : (
43 |               'No Documentation'
44 |             )}
45 |            
46 |          
47 |       )
48 |     );
49 |   };
50 | 
51 |   return (
52 |     
53 |       
54 |         
55 |           Service Name 
56 |           Endpoints 
57 |           Documentation 
58 |          
59 |         {renderServiceRows()} 
60 |       
61 |     
 
62 |   );
63 | }
64 | 
65 | export default MainPageTable;
66 | 
--------------------------------------------------------------------------------
/src/css/json_theme.css:
--------------------------------------------------------------------------------
 1 | .__json-pretty__ {
 2 |   line-height: 1.3;
 3 |   color: #9ca3af;
 4 |   overflow: auto;
 5 | }
 6 | 
 7 | .__json-pretty__ .__json-key__ {
 8 |   color: #ff5e5e;
 9 | }
10 | 
11 | .__json-pretty__ .__json-value__ {
12 |   color: #fdb082;
13 | }
14 | 
15 | .__json-pretty__ .__json-string__ {
16 |   color: #168f46;
17 | }
18 | 
19 | .__json-pretty__ .__json-boolean__ {
20 |   color: #69c;
21 | }
22 | 
23 | .__json-pretty-error__ {
24 |   line-height: 1.3;
25 |   color: #9ca3af;
26 |   overflow: auto;
27 | }
28 | 
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
 1 | @tailwind base;
 2 | @tailwind components;
 3 | @tailwind utilities;
 4 | 
 5 | /* Disable Flowbite/Tailwind focus rings globally */
 6 | [multiple]:focus,
 7 | [type="date"]:focus,
 8 | [type="datetime-local"]:focus,
 9 | [type="email"]:focus,
10 | [type="month"]:focus,
11 | [type="number"]:focus,
12 | [type="password"]:focus,
13 | [type="search"]:focus,
14 | [type="tel"]:focus,
15 | [type="text"]:focus,
16 | [type="time"]:focus,
17 | [type="url"]:focus,
18 | [type="week"]:focus,
19 | select:focus,
20 | textarea:focus,
21 | button:focus,
22 | a:focus {
23 |   --tw-ring-inset: initial !important;
24 |   --tw-ring-offset-width: 0px !important;
25 |   --tw-ring-offset-color: transparent !important;
26 |   --tw-ring-color: transparent !important;
27 |   --tw-ring-offset-shadow: none !important;
28 |   --tw-ring-shadow: none !important;
29 |   border-color: inherit !important;
30 |   box-shadow: none !important;
31 |   outline: none !important;
32 |   outline-offset: 0 !important;
33 | }
34 | 
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
 1 | import React from 'react';
 2 | import ReactDOM from 'react-dom/client';
 3 | import App from './App.jsx';
 4 | import './index.css';
 5 | 
 6 | ReactDOM.createRoot(document.getElementById('root')).render(
 7 |   
 8 |      
 9 |    
10 | );
11 | 
--------------------------------------------------------------------------------
/src/modules/universal.jsx:
--------------------------------------------------------------------------------
  1 | import { useState, useEffect } from 'react';
  2 | import { useHash } from 'react-use';
  3 | import {
  4 |   Button,
  5 |   Dropdown,
  6 |   Label,
  7 |   TextInput,
  8 |   Alert,
  9 |   Tooltip,
 10 |   Checkbox,
 11 | } from 'flowbite-react';
 12 | import OutputWindow from '../components/response_window';
 13 | import RequestWindow from '../components/request_window';
 14 | import { HiInformationCircle } from 'react-icons/hi';
 15 | import makeUniversalRequest from '../components/requests';
 16 | import { AiOutlineLoading } from 'react-icons/ai';
 17 | 
 18 | export default function UniversalComponent({ serviceType, servicesConfig }) {
 19 |   const [hash, setHash] = useHash();
 20 |   const serviceConfig = servicesConfig[serviceType] || {};
 21 |   const apiDocumentationPage = serviceConfig.api_documentation_page;
 22 |   const endpoints = serviceConfig.endpoints || {};
 23 |   const defaultInputFields = serviceConfig.input_fields || {};
 24 | 
 25 |   const firstEndpointKey = Object.keys(endpoints)[0];
 26 |   const firstEndpoint = endpoints[firstEndpointKey] || {};
 27 | 
 28 |   const [selectedEndpoint, setSelectedEndpoint] = useState(
 29 |     firstEndpoint.label || 'Select Endpoint'
 30 |   );
 31 |   const [curlCommand, setCurlCommand] = useState(firstEndpoint.curl || '');
 32 |   const [requestURL, setRequestURL] = useState(firstEndpoint.request_url || '');
 33 |   const [requestMethod, setRequestMethod] = useState(
 34 |     firstEndpoint.request_method || ''
 35 |   );
 36 |   const [inputFields, setInputFields] = useState(
 37 |     firstEndpoint.override_default_input_field
 38 |       ? firstEndpoint.input_fields
 39 |       : defaultInputFields
 40 |   );
 41 |   const [status_code, setStatusCode] = useState(0);
 42 |   const [output_str, setOutputStr] = useState('');
 43 |   const [inputValues, setInputValues] = useState({});
 44 |   const [loading, setLoading] = useState(false);
 45 |   const [isChecked, setIsChecked] = useState(false);
 46 |   const colorIsFailure = serviceConfig?.alert?.color === 'failure';
 47 |   const enableButton = !colorIsFailure || (colorIsFailure && isChecked);
 48 | 
 49 |   const handleTestEndpoint = async () => {
 50 |     setLoading(true);
 51 |     try {
 52 |       const { status, data } = await makeUniversalRequest(
 53 |         isChecked,
 54 |         serviceType,
 55 |         inputValues,
 56 |         requestURL,
 57 |         requestMethod
 58 |       );
 59 |       setStatusCode(status);
 60 |       setOutputStr(JSON.stringify(data, null, 2));
 61 |     } catch (error) {
 62 |       console.error('Error:', error);
 63 |     } finally {
 64 |       setLoading(false);
 65 |     }
 66 |   };
 67 | 
 68 |   const generateDynamicCurl = (curlCommandTemplate, inputs) => {
 69 |     let dynamicCurl = curlCommandTemplate;
 70 |     Object.keys(inputs).forEach((key) => {
 71 |       dynamicCurl = dynamicCurl.replace(`<${key}>`, inputs[key]);
 72 |     });
 73 |     return dynamicCurl;
 74 |   };
 75 | 
 76 |   const handleDropdownChange = (endpointKey, preserveHash = false) => {
 77 |     const endpointConfig = endpoints[endpointKey];
 78 |     setSelectedEndpoint(endpointConfig.label);
 79 |     const updatedCurl = generateDynamicCurl(endpointConfig.curl, inputValues);
 80 |     setCurlCommand(updatedCurl);
 81 |     setRequestURL(endpointConfig.request_url);
 82 |     setRequestMethod(endpointConfig.request_method);
 83 | 
 84 |     if (endpointConfig.override_default_input_field) {
 85 |       setInputFields(endpointConfig.input_fields || {});
 86 |     } else {
 87 |       setInputFields(defaultInputFields);
 88 |     }
 89 | 
 90 |     // Only update hash if we're not preserving it
 91 |     if (!preserveHash) {
 92 |       const updatedInputs = { ...inputValues, endpoint: endpointKey };
 93 |       setHash(new URLSearchParams(updatedInputs).toString());
 94 |     }
 95 |   };
 96 | 
 97 |   const handleInputChange = (key, value) => {
 98 |     setInputValues((prev) => {
 99 |       const updatedInputs = { ...prev, [key]: value };
100 |       const updatedCurl = generateDynamicCurl(curlCommand, updatedInputs);
101 |       setCurlCommand(updatedCurl);
102 |       // Include the current endpoint in the hash
103 |       const hashInputs = { ...updatedInputs, endpoint: Object.keys(endpoints).find(key => endpoints[key].label === selectedEndpoint) };
104 |       setHash(new URLSearchParams(hashInputs).toString());
105 |       return updatedInputs;
106 |     });
107 |   };
108 | 
109 |   const handleCheckboxChange = (e) => {
110 |     setIsChecked(e.target.checked);
111 |   };
112 | 
113 |   useEffect(() => {
114 |     // Only reset state variables when serviceType changes
115 |     const newServiceConfig = servicesConfig[serviceType] || {};
116 |     const newEndpoints = newServiceConfig.endpoints || {};
117 |     const newDefaultInputFields = newServiceConfig.input_fields || {};
118 | 
119 |     const firstNewEndpointKey = Object.keys(newEndpoints)[0];
120 |     const firstNewEndpoint = newEndpoints[firstNewEndpointKey] || {};
121 | 
122 |     // Reset the selected endpoint and input fields only when serviceType changes
123 |     setSelectedEndpoint(firstNewEndpoint.label || 'Select Endpoint');
124 |     setRequestURL(firstNewEndpoint.request_url || '');
125 |     setRequestMethod(firstNewEndpoint.request_method || '');
126 |     setInputFields(
127 |       firstNewEndpoint.override_default_input_field
128 |         ? firstNewEndpoint.input_fields
129 |         : newDefaultInputFields
130 |     );
131 | 
132 |     // Reset status code when serviceType changes
133 |     setStatusCode(0);
134 |   }, [serviceType]); // Only depend on serviceType
135 | 
136 |   useEffect(() => {
137 |     // Update the curl content dynamically based on inputValues but don't reset the endpoint
138 |     const newServiceConfig = servicesConfig[serviceType] || {};
139 |     const newEndpoints = newServiceConfig.endpoints || {};
140 | 
141 |     // Get the currently selected endpoint
142 |     const selectedEndpointData =
143 |       Object.values(newEndpoints).find(
144 |         (endpoint) => endpoint.label === selectedEndpoint
145 |       ) || {};
146 | 
147 |     // Generate the updated cURL command based on the selected endpoint and current input values
148 |     const updatedCurl = generateDynamicCurl(
149 |       selectedEndpointData.curl,
150 |       inputValues
151 |     );
152 |     setCurlCommand(updatedCurl);
153 |   }, [inputValues, selectedEndpoint, serviceType]); // Depend on inputValues, selectedEndpoint, and serviceType
154 | 
155 |   useEffect(() => {
156 |     if (hash) {
157 |       try {
158 |         const hashString = hash.substring(1); // Remove the # character
159 |         if (!hashString) return;
160 |         const hashParamsUrl = new URLSearchParams(hashString);
161 |         var _inputFields = inputFields;
162 |         // First handle the endpoint if it exists
163 |         const endpointKey = hashParamsUrl.get('endpoint');
164 |         if (endpointKey && endpoints[endpointKey]) {
165 |           handleDropdownChange(endpointKey, true); // Pass true to preserve hash
166 |           _inputFields = endpoints[endpointKey].input_fields ?? inputFields;
167 |         }
168 | 
169 |         // Then handle the input fields
170 |         Object.keys(_inputFields).forEach((key) => {
171 |           const value = hashParamsUrl.get(key);
172 |           if (value) {
173 |             setInputValues((prev) => ({ ...prev, [key]: value }));
174 |           }
175 |         });
176 |       } catch (error) {
177 |         console.error('Error parsing hash:', error);
178 |       }
179 |     }
180 |   }, [hash]);
181 | 
182 |   return (
183 |     
184 |       {apiDocumentationPage ? (
185 |         
 👈🏻 Click to View Official API Documentation}
187 |           placement="right"
188 |         >
189 |           
195 |             Check {serviceType} Keys
196 |            
197 |          
198 |       ) : (
199 |         
200 |           Check {serviceType} Keys
201 |          
202 |       )}
203 |       
204 |         {Object.keys(inputFields).map((key) => (
205 |           
206 |             
207 |                
208 |             
209 |             
 handleInputChange(key, e.target.value)}
215 |               required
216 |             />
217 |            
218 |         ))}
219 | 
220 |         {serviceConfig.alert && (
221 |           
222 |             Alert! {' '}
223 |             {serviceConfig.alert.alert_message}
224 |            
225 |         )}
226 | 
227 |         
228 |           {serviceConfig?.alert?.color === 'failure' && (
229 |             
230 |                
236 |               
237 |                 Use SecretsNinja Proxy to bypass CORS
238 |                
239 |             
240 |           )}
241 | 
242 |           
243 |           
244 |             
245 |               
246 |                 {Object.keys(endpoints).map((key) => (
247 |                    handleDropdownChange(key)}
250 |                   >
251 |                     {endpoints[key].label}
252 |                    
253 |                 ))}
254 |               
255 |              
256 |             
262 |               }
263 |               disabled={!enableButton}
264 |             >
265 |               Test Endpoint
266 |             
267 |           
268 |         
269 |       
270 |       
275 |       
276 |          
277 |       
278 |     
 
279 |   );
280 | }
281 | 
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
 1 | /** @type {import('tailwindcss').Config} */
 2 | export default {
 3 |   content: [
 4 |     './src/**/*.{js,jsx,ts,tsx}',
 5 |     'node_modules/flowbite-react/lib/esm/**/*.js',
 6 |   ],
 7 |   theme: {
 8 |     extend: {},
 9 |   },
10 |   plugins: [require('flowbite/plugin')],
11 | };
12 | 
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 | 
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 |   plugins: [react()],
7 | });
8 | 
--------------------------------------------------------------------------------