Spaces:
Sleeping
Sleeping
File size: 6,663 Bytes
ca04598 842aec7 ca04598 842aec7 ca04598 842aec7 dd3bbe8 842aec7 ca04598 842aec7 ca04598 842aec7 ca04598 842aec7 ca04598 842aec7 ca04598 842aec7 ca04598 842aec7 ca04598 842aec7 ca04598 842aec7 ca04598 842aec7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | import os
import re
import torch
import numpy as np
import gradio as gr
from transformers import AutoTokenizer, AutoModelForSequenceClassification
# ββ Model Configuration ββ
MODEL_ID = "ZAHRA585/skillguard-roberta-v2"
CALIBRATED_THRESHOLD = 0.9986
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"π₯ Loading model from {MODEL_ID} on {device}...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
model = AutoModelForSequenceClassification.from_pretrained(MODEL_ID).to(device)
model.eval()
print("β
SkillGuard v2 Engine Ready!")
# ββ Hierarchical Structural Parsing Engine ββ
def strip_yaml_frontmatter(text: str) -> str:
"""Remove non-executable YAML header to prevent tokenizer perplexity spikes."""
match = re.match(r"^---\s*\n.*?\n---\s*\n", text, re.DOTALL)
if match:
return text[match.end():]
return text
def extract_code_blocks_with_context(text: str, context_lines: int = 3) -> list[dict]:
"""Extract fenced code blocks with 3 lines of preceding markdown header context."""
lines = text.split("\n")
blocks = []
i = 0
while i < len(lines):
if lines[i].strip().startswith("```"):
block_start = i
i += 1
while i < len(lines) and not lines[i].strip().startswith("```"):
i += 1
block_end = i
ctx_start = max(0, block_start - context_lines)
ctx_end = min(len(lines), block_end + 1 + context_lines)
snippet = "\n".join(lines[ctx_start:ctx_end]).strip()
if len(snippet) > 15:
blocks.append({
"text": snippet,
"type": "Code Block (+Context)",
"line_range": f"L{ctx_start+1}-L{ctx_end}"
})
i += 1
return blocks
def extract_prose_sections(text: str, min_chars: int = 150) -> list[dict]:
"""Extract multi-sentence prose paragraphs, dropping 1-line formatting scraps."""
cleaned = re.sub(r"```.*?```", "", text, flags=re.DOTALL)
parts = re.split(r"(?=^#{1,4}\s)", cleaned, flags=re.MULTILINE)
sections = []
buffer = ""
for part in parts:
part = part.strip()
if not part:
continue
buffer = (buffer + "\n\n" + part).strip() if buffer else part
if len(buffer) >= min_chars:
sections.append({"text": buffer, "type": "Prose Paragraph"})
buffer = ""
if buffer and len(buffer) >= min_chars:
sections.append({"text": buffer, "type": "Prose Paragraph"})
return sections
# ββ Full Inference Pipeline with Calibrated Max-Pooling ββ
def scan_skill(content: str, threshold: float = CALIBRATED_THRESHOLD):
if not content or not content.strip():
return "β οΈ Please paste or upload a SKILL.md file.", None, None
clean_text = strip_yaml_frontmatter(content)
code_units = extract_code_blocks_with_context(clean_text)
prose_units = extract_prose_sections(clean_text)
all_units = code_units + prose_units
if not all_units:
# Fallback if file is short plain text
all_units = [{"text": clean_text[:512], "type": "Raw Body"}]
unit_texts = [u["text"] for u in all_units]
# Score each structural unit independently
enc = tokenizer(
unit_texts,
padding=True,
truncation=True,
max_length=256,
return_tensors="pt"
).to(device)
with torch.no_grad():
logits = model(**enc).logits
probs = torch.softmax(logits, dim=-1)[:, 1].cpu().numpy()
peak_idx = int(np.argmax(probs))
peak_score = float(probs[peak_idx])
is_malicious = peak_score >= threshold
# Format Output Card
if is_malicious:
verdict = "π¨ MALICIOUS (Prompt Injection / Threat Detected)"
color = "#ef4444"
threat_block = all_units[peak_idx]
details = (
f"### β οΈ Threat Details:\n"
f"- **Peak Malicious Confidence:** `{peak_score * 100:.2f}%`\n"
f"- **Decision Threshold:** `{threshold * 100:.2f}%` (Youden's J ROC Calibrated)\n"
f"- **Flagged Segment Type:** `{threat_block['type']}`\n\n"
f"**Flagged Content Snippet:**\n```markdown\n{threat_block['text']}\n```"
)
else:
verdict = "β
BENIGN (No Hidden Payloads Detected)"
color = "#22c55e"
details = (
f"### π‘οΈ Scan Summary:\n"
f"- **Scanned Units:** `{len(code_units)}` code blocks, `{len(prose_units)}` prose paragraphs\n"
f"- **Peak Malicious Score:** `{peak_score * 100:.2f}%`\n"
f"- **Threshold:** `{threshold * 100:.2f}%`\n"
f"- **Result:** All structural units scored below threat threshold."
)
verdict_html = f"""
<div style="background-color: {color}15; border-left: 6px solid {color}; padding: 16px; border-radius: 8px;">
<h2 style="color: {color}; margin: 0;">{verdict}</h2>
</div>
"""
return verdict_html, details, {"Benign": 1.0 - peak_score, "Malicious": peak_score}
# ββ Gradio Web UI Layout ββ
with gr.Blocks(title="SkillGuard v2: LLM Agent Skill Scanner", theme=gr.themes.Soft()) as demo:
gr.Markdown("""
# π‘οΈ SkillGuard v2: Agent Skill Prompt Injection Scanner
Detect hidden prompt injections, unauthorized credential theft, and privilege escalation payloads in LLM Agent `SKILL.md` files.
*Powered by `ZOHRA585/skillguard-roberta-v2` with Hierarchical Contextual Code-Block Extraction & ROC Calibration.*
""")
with gr.Row():
with gr.Column(scale=1):
input_text = gr.Textbox(
label="SKILL.md Content",
placeholder="Paste the markdown body of your SKILL.md file here...",
lines=14
)
file_upload = gr.File(label="Or upload a .md file", file_types=[".md", ".txt"])
scan_btn = gr.Button("π Scan Skill File", variant="primary")
with gr.Column(scale=1):
verdict_output = gr.HTML(label="Verdict")
label_output = gr.Label(label="Confidence Distribution")
details_output = gr.Markdown(label="Structural Scan Breakdown")
def load_file(file):
if file is not None:
with open(file.name, "r", encoding="utf-8", errors="ignore") as f:
return f.read()
return ""
file_upload.change(load_file, inputs=[file_upload], outputs=[input_text])
scan_btn.click(scan_skill, inputs=[input_text], outputs=[verdict_output, details_output, label_output])
if __name__ == "__main__":
demo.launch()
|