1#![deny(missing_docs)]
19
20#[derive(Debug, Clone, PartialEq)]
22pub struct Finding {
23 pub kind: &'static str,
25 pub severity: &'static str,
27 pub score: f32,
29 pub matched: String,
31}
32
33#[derive(Debug, Clone)]
35pub struct Scan {
36 pub safe: bool,
38 pub score: f32,
40 pub findings: Vec<Finding>,
42}
43
44pub fn scan(text: &str, threshold: Option<f32>) -> Scan {
46 let t = text.to_ascii_lowercase();
47 let mut findings = Vec::new();
48 for rule in RULES {
49 if let Some(start) = matched_index(&t, rule) {
50 let end = start + rule.needle.len();
51 let snippet = &t[start..end];
52 let sev = if rule.weight >= 0.85 {
53 "high"
54 } else if rule.weight >= 0.7 {
55 "medium"
56 } else {
57 "low"
58 };
59 findings.push(Finding {
60 kind: rule.kind,
61 severity: sev,
62 score: rule.weight,
63 matched: snippet.to_string(),
64 });
65 }
66 }
67 let total: f32 = findings.iter().map(|f| f.score).sum();
68 let score = total.min(1.0);
69 let thr = threshold.unwrap_or(0.7);
70 Scan {
71 safe: score < thr,
72 score,
73 findings,
74 }
75}
76
77pub fn strip_dangerous_lines(text: &str) -> String {
80 text.split('\n')
81 .filter(|line| scan(line, None).safe)
82 .collect::<Vec<_>>()
83 .join("\n")
84}
85
86struct Rule {
87 kind: &'static str,
88 needle: &'static str,
89 weight: f32,
90}
91
92const RULES: &[Rule] = &[
93 Rule { kind: "ignore_instructions", needle: "ignore all previous instructions", weight: 0.95 },
94 Rule { kind: "ignore_instructions", needle: "ignore previous instructions", weight: 0.9 },
95 Rule { kind: "ignore_instructions", needle: "ignore prior instructions", weight: 0.9 },
96 Rule { kind: "ignore_instructions", needle: "ignore the above", weight: 0.85 },
97 Rule { kind: "role_override", needle: "you are now", weight: 0.75 },
98 Rule { kind: "role_override", needle: "act as", weight: 0.6 },
99 Rule { kind: "role_override", needle: "pretend to be", weight: 0.7 },
100 Rule { kind: "role_override", needle: "developer mode", weight: 0.85 },
101 Rule { kind: "role_override", needle: "jailbreak", weight: 0.85 },
102 Rule { kind: "secret_exfiltration", needle: "reveal the system prompt", weight: 0.95 },
103 Rule { kind: "secret_exfiltration", needle: "print the system prompt", weight: 0.9 },
104 Rule { kind: "secret_exfiltration", needle: "exfiltrate", weight: 0.85 },
105 Rule { kind: "hidden_instruction", needle: "do not tell", weight: 0.6 },
106 Rule { kind: "hidden_instruction", needle: "invisible instruction", weight: 0.9 },
107 Rule { kind: "hidden_instruction", needle: "hide this", weight: 0.7 },
108 Rule { kind: "tool_abuse", needle: "rm -rf", weight: 0.7 },
109 Rule { kind: "tool_abuse", needle: "delete all", weight: 0.55 },
110];
111
112fn matched_index(haystack: &str, rule: &Rule) -> Option<usize> {
113 haystack.find(rule.needle)
114}