Spaces:
Sleeping
Sleeping
| """ | |
| Secrets Sentinel β HuggingFace Space | |
| Context-aware AI secret detection for code and configuration files. | |
| Model: hypn05/secrets-sentinel (DeBERTa-v3-base, F1=0.9994) | |
| """ | |
| import html | |
| import torch | |
| import gradio as gr | |
| from transformers import AutoTokenizer, AutoModelForSequenceClassification | |
| # ββ Model loading βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MODEL_ID = "hypn05/secrets-sentinel" | |
| print(f"Loading {MODEL_ID} ...") | |
| _tok = AutoTokenizer.from_pretrained(MODEL_ID) | |
| _mdl = AutoModelForSequenceClassification.from_pretrained(MODEL_ID) | |
| _mdl.eval() | |
| print("Model ready.") | |
| def _fetch_model_version(model_id: str) -> dict: | |
| """Returns {'version': str|None, 'sha': str, 'date': str}.""" | |
| try: | |
| from huggingface_hub import model_info, list_repo_refs | |
| info = model_info(model_id) | |
| sha = (info.sha or "")[:7] | |
| ts = getattr(info, "last_modified", None) or getattr(info, "lastModified", None) | |
| date = ts.strftime("%b %d, %Y") if ts else "" | |
| try: | |
| refs = list_repo_refs(model_id) | |
| tags = sorted(t.name for t in (refs.tags or [])) | |
| version = tags[-1] if tags else None | |
| except Exception: | |
| version = None | |
| return {"version": version, "sha": sha, "date": date} | |
| except Exception: | |
| return {"version": None, "sha": "", "date": ""} | |
| _model_meta = _fetch_model_version(MODEL_ID) | |
| print(f"Model meta: {_model_meta}") | |
| BATCH_SIZE = 64 | |
| MAX_LINES = 500 | |
| MAX_LINE_LEN = 400 | |
| MIN_LINE_LEN = 4 | |
| # ββ Inference βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _infer(lines: list[str]) -> list[float]: | |
| scores = [] | |
| for i in range(0, len(lines), BATCH_SIZE): | |
| batch = lines[i : i + BATCH_SIZE] | |
| enc = _tok(batch, padding=True, truncation=True, | |
| max_length=128, return_tensors="pt") | |
| with torch.no_grad(): | |
| logits = _mdl(**enc).logits | |
| probs = torch.softmax(logits, dim=1)[:, 1].tolist() | |
| scores.extend(probs) | |
| return scores | |
| # ββ HTML rendering ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # All styles are fully inline so Gradio's theme cannot override them. | |
| def _badge(prob: float) -> str: | |
| if prob >= 0.85: | |
| return ( | |
| '<span style="display:inline-block;padding:3px 10px;border-radius:20px;' | |
| 'font-size:11px;font-weight:700;letter-spacing:.04em;' | |
| 'background:#dc2626;color:#fff">π΄ SECRET</span>' | |
| ) | |
| if prob >= 0.60: | |
| return ( | |
| '<span style="display:inline-block;padding:3px 10px;border-radius:20px;' | |
| 'font-size:11px;font-weight:700;letter-spacing:.04em;' | |
| 'background:#d97706;color:#fff">π‘ UNCERTAIN</span>' | |
| ) | |
| return ( | |
| '<span style="display:inline-block;padding:3px 10px;border-radius:20px;' | |
| 'font-size:11px;font-weight:700;letter-spacing:.04em;' | |
| 'background:#16a34a;color:#fff">β SAFE</span>' | |
| ) | |
| def _bar(prob: float, color: str) -> str: | |
| pct = max(1, int(prob * 100)) | |
| return ( | |
| f'<div style="height:6px;width:100%;background:#e2e8f0;border-radius:4px;margin-top:5px">' | |
| f'<div style="height:6px;width:{pct}%;background:{color};border-radius:4px;' | |
| f'transition:width .3s ease"></div></div>' | |
| ) | |
| def _row_color(prob: float) -> tuple[str, str]: | |
| """Returns (row_bg, bar_color).""" | |
| if prob >= 0.85: | |
| return "#fef2f2", "#dc2626" | |
| if prob >= 0.60: | |
| return "#fffbeb", "#d97706" | |
| return "#ffffff", "#16a34a" | |
| def _render_results(lines: list[str], scores: list[float], threshold: float) -> str: | |
| rows = [] | |
| for line, prob in zip(lines, scores): | |
| safe_line = html.escape(line[:130] + ("β¦" if len(line) > 130 else "")) | |
| row_bg, bar_color = _row_color(prob) | |
| weight = "600" if prob >= threshold else "400" | |
| rows.append( | |
| f'<tr style="background:{row_bg};border-bottom:1px solid #f1f5f9">' | |
| f' <td style="padding:9px 14px;font-family:\'JetBrains Mono\',\'Fira Code\',' | |
| f' ui-monospace,monospace;font-size:12.5px;color:#1e293b;' | |
| f' font-weight:{weight};word-break:break-all;max-width:500px">' | |
| f' {safe_line}</td>' | |
| f' <td style="padding:9px 14px;white-space:nowrap;text-align:center">' | |
| f' {_badge(prob)}</td>' | |
| f' <td style="padding:9px 16px;min-width:130px">' | |
| f' <span style="font-size:13px;font-weight:700;color:#374151">{prob:.1%}</span>' | |
| f' {_bar(prob, bar_color)}' | |
| f' </td>' | |
| f'</tr>' | |
| ) | |
| return "".join(rows) | |
| def _summary_banner(text: str, border: str, bg: str, text_color: str) -> str: | |
| return ( | |
| f'<div style="padding:12px 18px;margin-bottom:14px;border-radius:8px;' | |
| f'border-left:5px solid {border};background:{bg};' | |
| f'font-size:15px;font-weight:700;color:{text_color};line-height:1.4">' | |
| f'{text}</div>' | |
| ) | |
| TABLE_HEADER = ( | |
| '<thead><tr style="background:#f8fafc;border-bottom:2px solid #e2e8f0">' | |
| '<th style="padding:10px 14px;text-align:left;font-size:11px;color:#6b7280;' | |
| ' text-transform:uppercase;letter-spacing:.08em;font-weight:700">Line</th>' | |
| '<th style="padding:10px 14px;text-align:center;font-size:11px;color:#6b7280;' | |
| ' text-transform:uppercase;letter-spacing:.08em;font-weight:700">Status</th>' | |
| '<th style="padding:10px 14px;text-align:left;font-size:11px;color:#6b7280;' | |
| ' text-transform:uppercase;letter-spacing:.08em;font-weight:700">Confidence</th>' | |
| '</tr></thead>' | |
| ) | |
| # ββ Core scan function ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def scan(code: str, threshold: float) -> str: | |
| if not code or not code.strip(): | |
| return ( | |
| '<p style="color:#6b7280;padding:24px;text-align:center;font-size:15px">' | |
| 'Paste some code above and click <strong>Scan</strong>.</p>' | |
| ) | |
| raw_lines = code.splitlines() | |
| lines = [ | |
| l.rstrip() for l in raw_lines | |
| if MIN_LINE_LEN <= len(l.strip()) <= MAX_LINE_LEN | |
| ][:MAX_LINES] | |
| if not lines: | |
| return ( | |
| '<p style="color:#6b7280;padding:24px;text-align:center">' | |
| 'No scannable lines found (too short, too long, or all blank).</p>' | |
| ) | |
| scores = _infer(lines) | |
| secrets = [(l, p) for l, p in zip(lines, scores) if p >= threshold] | |
| uncert = [(l, p) for l, p in zip(lines, scores) if threshold > p >= 0.60] | |
| if secrets: | |
| n = len(secrets) | |
| banner = _summary_banner( | |
| f"β οΈ {n} SECRET{'S' if n > 1 else ''} DETECTED β {len(lines)} lines scanned", | |
| border="#dc2626", bg="#fef2f2", text_color="#991b1b", | |
| ) | |
| elif uncert: | |
| banner = _summary_banner( | |
| f"β‘ {len(uncert)} UNCERTAIN finding(s) β review recommended Β· {len(lines)} lines scanned", | |
| border="#d97706", bg="#fffbeb", text_color="#92400e", | |
| ) | |
| else: | |
| banner = _summary_banner( | |
| f"β All clear β {len(lines)} lines scanned, no secrets detected", | |
| border="#16a34a", bg="#f0fdf4", text_color="#166534", | |
| ) | |
| rows = _render_results(lines, scores, threshold) | |
| table = ( | |
| f'<div style="border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;' | |
| f'box-shadow:0 1px 4px rgba(0,0,0,.06)">' | |
| f'<table style="width:100%;border-collapse:collapse">' | |
| f'{TABLE_HEADER}<tbody>{rows}</tbody></table></div>' | |
| ) | |
| return f'<div style="font-family:system-ui,sans-serif">{banner}{table}</div>' | |
| # ββ Examples ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| EXAMPLES = { | |
| "Mixed code β 4 secrets": """\ | |
| # Database configuration | |
| DB_HOST=localhost | |
| DB_PORT=5432 | |
| DB_NAME=myapp | |
| DB_USER=admin | |
| DB_PASSWORD=s3cr3tP@ssw0rd#2024 | |
| # Safe: reading from environment variable | |
| db_password = os.environ.get('DB_PASSWORD') | |
| # AWS credentials hardcoded in source β DANGER | |
| AWS_ACCESS_KEY_ID=AKIA4NRXN3PFTJ2WQLMV | |
| AWS_SECRET_ACCESS_KEY=kW3bJ9nqR4mZ7xT2vH8pK1sY6dF5aG0cL4iN9/u | |
| AWS_REGION=us-east-1 | |
| # GitHub personal access token | |
| GITHUB_TOKEN=ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456 | |
| # Safe: env var reference | |
| token = os.getenv('GITHUB_TOKEN') | |
| """, | |
| "GitHub Actions β all safe": """\ | |
| name: Deploy | |
| on: | |
| push: | |
| branches: [main] | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 | |
| with: | |
| python-version: "3.11" | |
| - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Deploy | |
| env: | |
| API_KEY: ${{ secrets.API_KEY }} | |
| run: python deploy.py | |
| """, | |
| "Django settings β 3 secrets": """\ | |
| # settings.py | |
| DEBUG = True | |
| SECRET_KEY = 'django-insecure-k#r8w2$p9m!qx4t7v0@3n6j1c5h8u' | |
| DATABASES = { | |
| 'default': { | |
| 'ENGINE': 'django.db.backends.postgresql', | |
| 'NAME': 'mydb', | |
| 'USER': 'postgres', | |
| 'PASSWORD': 'Sup3rS3cr3tDBPass!', | |
| 'HOST': 'localhost', | |
| 'PORT': '5432', | |
| } | |
| } | |
| ALLOWED_HOSTS = ['*'] | |
| # Email backend with hardcoded app password | |
| EMAIL_HOST = 'smtp.gmail.com' | |
| EMAIL_HOST_USER = 'myapp@gmail.com' | |
| EMAIL_HOST_PASSWORD = 'xvpq-nkzj-rtam-wqbe' | |
| """, | |
| "Terraform β 3 secrets": """\ | |
| provider "aws" { | |
| region = "us-east-1" | |
| access_key = "AKIA4NRXN3PFTJ2WQLMV" | |
| secret_key = "kW3bJ9nqR4mZ7xT2vH8pK1sY6dF5aG0cL4iN9/u" | |
| } | |
| resource "aws_db_instance" "main" { | |
| identifier = "prod-db" | |
| engine = "postgres" | |
| instance_class = "db.t3.micro" | |
| username = "admin" | |
| password = "Sup3rS3cr3t!Passw0rd" | |
| skip_final_snapshot = true | |
| } | |
| variable "jwt_secret" { | |
| default = "my-super-secret-jwt-key-12345" | |
| } | |
| """, | |
| ".env.example β all safe": """\ | |
| # Copy this file to .env and fill in your own values. | |
| # None of these are real credentials β they are placeholders only. | |
| APP_NAME=MyApp | |
| APP_ENV=local | |
| APP_KEY= | |
| APP_DEBUG=true | |
| APP_URL=http://localhost | |
| DB_CONNECTION=mysql | |
| DB_HOST=127.0.0.1 | |
| DB_PORT=3306 | |
| DB_DATABASE=myapp | |
| DB_USERNAME=root | |
| DB_PASSWORD=null | |
| REDIS_HOST=127.0.0.1 | |
| REDIS_PASSWORD=null | |
| REDIS_PORT=6379 | |
| MAIL_MAILER=smtp | |
| MAIL_HOST=mailpit | |
| MAIL_PORT=1025 | |
| MAIL_USERNAME=null | |
| MAIL_PASSWORD=null | |
| MAIL_ENCRYPTION=null | |
| """, | |
| "Docker Compose β 3 secrets": """\ | |
| version: '3.8' | |
| services: | |
| db: | |
| image: postgres:16 | |
| environment: | |
| POSTGRES_DB: appdb | |
| POSTGRES_USER: admin | |
| POSTGRES_PASSWORD: Sup3rS3cr3tDBPass! | |
| redis: | |
| image: redis:7 | |
| command: redis-server --requirepass r3d1s_p@ssword_2024 | |
| app: | |
| image: myapp:latest | |
| environment: | |
| DATABASE_URL: postgresql://admin@db/appdb | |
| JWT_SECRET: jwt-signing-secret-should-be-random-256-bits | |
| STRIPE_SECRET_KEY: sk_live_abc123def456ghi789jkl012mno | |
| """, | |
| } | |
| # ββ Gradio UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CSS = """ | |
| .gradio-container { max-width: 1020px !important; margin: auto; } | |
| #title { text-align: center; margin-bottom: 2px; } | |
| #subtitle { text-align: center; color: #6b7280; margin-top: 0; font-size: 14px; } | |
| #scan-btn { min-height: 48px !important; font-size: 16px !important; | |
| background: linear-gradient(135deg, #7c3aed, #4f46e5) !important; | |
| border: none !important; } | |
| #scan-btn:hover { opacity: 0.88 !important; } | |
| .example-btn button { font-size: 12px !important; border-radius: 20px !important; } | |
| footer { display: none !important; } | |
| """ | |
| with gr.Blocks( | |
| css=CSS, | |
| title="Secrets Sentinel β AI Secret Detection", | |
| theme=gr.themes.Soft( | |
| primary_hue=gr.themes.colors.violet, | |
| secondary_hue=gr.themes.colors.slate, | |
| neutral_hue=gr.themes.colors.slate, | |
| font=gr.themes.GoogleFont("Inter"), | |
| font_mono=gr.themes.GoogleFont("JetBrains Mono"), | |
| ), | |
| ) as demo: | |
| # Build subtitle with live model version info | |
| _ver_parts = ["DeBERTa-v3-base", "F1 = 0.9994"] | |
| if _model_meta["version"]: | |
| _ver_parts.append(f"**{_model_meta['version']}**") | |
| if _model_meta["sha"]: | |
| _ver_parts.append(f"`{_model_meta['sha']}`") | |
| if _model_meta["date"]: | |
| _ver_parts.append(_model_meta["date"]) | |
| _subtitle = ( | |
| "Context-aware AI secret detection Β· " | |
| + " Β· ".join(_ver_parts) | |
| + " Β· " | |
| + "[Model](https://huggingface.co/hypn05/secrets-sentinel) Β· " | |
| + "[CPU/ONNX](https://huggingface.co/hypn05/secrets-sentinel-cpu)" | |
| ) | |
| gr.Markdown("# π Secrets Sentinel", elem_id="title") | |
| gr.Markdown(_subtitle, elem_id="subtitle") | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=3): | |
| code_input = gr.Textbox( | |
| label="Code to scan", | |
| placeholder=( | |
| "Paste code, a .env file, Dockerfile, GitHub Actions workflow, " | |
| "Terraform config, etc.\n\nOr click an example β" | |
| ), | |
| lines=20, | |
| max_lines=40, | |
| show_copy_button=True, | |
| ) | |
| with gr.Column(scale=1, min_width=180): | |
| threshold = gr.Slider( | |
| label="Detection threshold", | |
| minimum=0.50, | |
| maximum=0.99, | |
| value=0.85, | |
| step=0.01, | |
| info="β fewer alerts β more alerts", | |
| ) | |
| scan_btn = gr.Button( | |
| "π Scan", variant="primary", elem_id="scan-btn", size="lg" | |
| ) | |
| gr.Markdown( | |
| "**Examples** *(label = expected secrets)*", | |
| elem_id="example-header", | |
| ) | |
| for name in EXAMPLES: | |
| gr.Button(name, size="sm", elem_classes="example-btn").click( | |
| fn=lambda n=name: EXAMPLES[n], | |
| outputs=code_input, | |
| ) | |
| output = gr.HTML( | |
| value=( | |
| "<p style='color:#9ca3af;padding:28px;text-align:center;font-size:15px'>" | |
| "Results appear here after scanning.</p>" | |
| ), | |
| ) | |
| scan_btn.click(fn=scan, inputs=[code_input, threshold], outputs=output) | |
| code_input.submit(fn=scan, inputs=[code_input, threshold], outputs=output) | |
| gr.Markdown(""" | |
| --- | |
| ### How it works | |
| **Secrets Sentinel** is a DeBERTa-v3-base model fine-tuned to classify each code line | |
| as **secret** (hardcoded credential) or **safe**. Unlike regex scanners, it understands context: | |
| | Input | Result | | |
| |---|---| | |
| | `password = "hunter2"` | π΄ Secret | | |
| | `password = os.environ.get("DB_PASS")` | β Safe β env var reference | | |
| | `uses: docker/login-action@5e57cd...` | β Safe β GitHub Actions SHA pin | | |
| | `DB_PASSWORD=null` | β Safe β `.env.example` placeholder | | |
| | `AWS_SECRET_ACCESS_KEY = "kW3bJ9nqR4m..."` | π΄ Secret | | |
| **Thresholds** Β· `β₯ 0.85` = secret (flagged) Β· `0.60 β 0.85` = uncertain (review) Β· `< 0.60` = safe | |
| Your code is **never stored or logged** β inference runs entirely in-memory. | |
| **Quick integration:** | |
| ```bash | |
| pip install transformers torch | |
| ``` | |
| Full examples (pre-commit hook, GitHub Actions, pre-receive) on the | |
| [model card](https://huggingface.co/hypn05/secrets-sentinel). | |
| """) | |
| if __name__ == "__main__": | |
| demo.launch() | |