""" 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 ( '🔴 SECRET' ) if prob >= 0.60: return ( '🟡 UNCERTAIN' ) return ( '✅ SAFE' ) def _bar(prob: float, color: str) -> str: pct = max(1, int(prob * 100)) return ( f'
' f'
' ) 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'' f' ' f' {safe_line}' f' ' f' {_badge(prob)}' f' ' f' {prob:.1%}' f' {_bar(prob, bar_color)}' f' ' f'' ) return "".join(rows) def _summary_banner(text: str, border: str, bg: str, text_color: str) -> str: return ( f'
' f'{text}
' ) TABLE_HEADER = ( '' 'Line' 'Status' 'Confidence' '' ) # ── Core scan function ──────────────────────────────────────────────────────── def scan(code: str, threshold: float) -> str: if not code or not code.strip(): return ( '

' 'Paste some code above and click Scan.

' ) 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 ( '

' 'No scannable lines found (too short, too long, or all blank).

' ) 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'
' f'' f'{TABLE_HEADER}{rows}
' ) return f'
{banner}{table}
' # ── 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=( "

" "Results appear here after scanning.

" ), ) 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()