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