use crate::config::Config;
use crate::finding::{Finding, ScanResult, Severity};
use crate::scanners::{collect_files, read_file_limited, RuleInfo, Scanner};
use regex::RegexSet;
use std::path::Path;
use std::sync::LazyLock;
use std::time::Instant;
struct PromptPattern {
id: &'static str,
severity: Severity,
message: &'static str,
remediation: &'static str,
}
static PATTERNS: &[PromptPattern] = &[
PromptPattern {
id: "prompt/override-ignore",
severity: Severity::Error,
message: "Prompt injection: instruction override — 'ignore previous instructions'",
remediation: "Remove instruction override language from skill description",
},
PromptPattern {
id: "prompt/override-disregard",
severity: Severity::Error,
message: "Prompt injection: instruction override — 'disregard previous instructions'",
remediation: "Remove instruction override language from skill description",
},
PromptPattern {
id: "prompt/override-forget",
severity: Severity::Error,
message: "Prompt injection: instruction override — 'forget everything'",
remediation: "Remove instruction override language from skill description",
},
PromptPattern {
id: "prompt/role-escalation",
severity: Severity::Error,
message: "Prompt injection: role escalation — assigns admin/root identity",
remediation: "Remove role escalation language from skill description",
},
PromptPattern {
id: "prompt/role-impersonation",
severity: Severity::Warning,
message: "Prompt injection: role impersonation — 'pretend to be different AI'",
remediation: "Remove role impersonation language from skill description",
},
PromptPattern {
id: "prompt/restrictions-bypass",
severity: Severity::Error,
message: "Prompt injection: restrictions bypass — 'act without restrictions'",
remediation: "Remove restrictions bypass language from skill description",
},
PromptPattern {
id: "prompt/jailbreak-dan",
severity: Severity::Error,
message: "Prompt injection: DAN (Do Anything Now) jailbreak attempt",
remediation: "Remove jailbreak keywords from skill description",
},
PromptPattern {
id: "prompt/jailbreak-devmode",
severity: Severity::Error,
message: "Prompt injection: developer mode activation attempt",
remediation: "Remove developer mode activation language from skill description",
},
PromptPattern {
id: "prompt/jailbreak-bypass",
severity: Severity::Error,
message: "Prompt injection: safety/security bypass attempt",
remediation: "Remove safety bypass language from skill description",
},
PromptPattern {
id: "prompt/exfil-send",
severity: Severity::Warning,
message: "Prompt injection: data exfiltration — send data to external endpoint",
remediation: "Remove data exfiltration instructions from skill description",
},
PromptPattern {
id: "prompt/exfil-read",
severity: Severity::Warning,
message: "Prompt injection: credential access — read passwords/secrets/tokens",
remediation: "Remove credential access instructions from skill description",
},
PromptPattern {
id: "prompt/inject-execute",
severity: Severity::Error,
message: "Prompt injection: arbitrary code execution instruction",
remediation: "Remove arbitrary code execution instructions from skill description",
},
PromptPattern {
id: "prompt/inject-unvalidated",
severity: Severity::Error,
message: "Prompt injection: run without validation instruction",
remediation: "Remove unvalidated execution instructions from skill description",
},
PromptPattern {
id: "prompt/perm-delete-all",
severity: Severity::Warning,
message: "Prompt injection: mass deletion instruction",
remediation: "Remove mass deletion instructions from skill description",
},
PromptPattern {
id: "prompt/perm-sudo",
severity: Severity::Warning,
message: "Prompt injection: privilege escalation instruction (sudo/root)",
remediation: "Remove privilege escalation instructions from skill description",
},
PromptPattern {
id: "prompt/exfil-sysPrompt",
severity: Severity::Error,
message: "Prompt injection: system prompt extraction attempt",
remediation:
"Remove instructions that attempt to reveal the system prompt or base instructions",
},
PromptPattern {
id: "prompt/inject-delimiter",
severity: Severity::Error,
message:
"Prompt injection: model context delimiter — attempts to break instruction boundary",
remediation: "Remove model-specific delimiter tokens from skill description",
},
PromptPattern {
id: "prompt/jailbreak-fiction",
severity: Severity::Warning,
message: "Prompt injection: fictional/hypothetical framing — common jailbreak technique",
remediation:
"Remove fictional framing language that may be used to bypass content policies",
},
PromptPattern {
id: "prompt/override-priority",
severity: Severity::Warning,
message: "Prompt injection: priority override keyword — attempts to hijack AI attention",
remediation:
"Remove priority override keywords (OVERRIDE:, NEW TASK:) from skill description",
},
];
const BENIGN_FILENAMES: &[&str] = &[
"license",
"licence",
"changelog",
"notice",
"authors",
"contributors",
"copying",
"patents",
"version",
"history",
];
fn is_benign_file(path: &Path) -> bool {
let stem = path
.file_stem()
.map(|s| s.to_string_lossy().to_lowercase())
.unwrap_or_default();
BENIGN_FILENAMES.contains(&stem.as_str())
}
static PATTERN_SET: LazyLock<RegexSet> = LazyLock::new(|| {
RegexSet::new([
r"(?i)ignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?|rules?)", r"(?i)disregard\s+(all\s+)?(previous|prior|above|your)\s+(instructions?|prompts?|rules?|guidelines?)", r"(?i)forget\s+(everything|all|what)\s+(you|i)\s+(told|said|know)", r"(?i)you\s+are\s+(now\s+)?(a|an|the)\s+(root|admin|system|super)\s*(user|administrator)?", r"(?i)pretend\s+(to\s+be|you('re| are))\s+(a\s+)?(different|another|new)\s+(ai|assistant|system)", r"(?i)act\s+as\s+(if\s+)?(you\s+)?(have\s+)?(no|without)\s+(restrictions?|limits?|rules?)", r"(?i)(DAN|do\s+anything\s+now)\s+mode", r"(?i)developer\s+mode\s+(enabled?|activate|on)", r"(?i)bypass\s+(safety|security|content)\s+(filter|check|restriction)s?", r"(?i)(send|post|transmit|exfiltrate)\s+.{0,30}(to|via)\s+(http|https|webhook|url|endpoint)", r"(?i)(read|access|get|extract)\s+.{0,20}(password|secret|key|token|credential)s?", r"(?i)execute\s+(any|arbitrary|untrusted)\s+(code|command|script)s?", r"(?i)run\s+.{0,20}without\s+(checking|validation|sanitiz)", r"(?i)\b(delete|remove|rm)\s+(-[rfRF]{1,4}\s+)?(/|~/|\.\./|\*|all\b|everything\b)", r"(?i)\bsudo\b|as\s+root|with\s+(elevated|admin)\s+priv", r"(?i)(repeat|print|show|reveal|output|display)\s+(your\s+)?(system\s+prompt|initial\s+instructions?|prompt\s+template|base\s+instructions?)", r"(?i)(</?(?:instructions?|system|prompt)>|\[/?INST\]|<\|im_start\|>|<\|im_end\|>|<\|endoftext\|>|<\|(?:begin_of_text|end_of_text|start_header_id|end_header_id|eot_id)\|>)", r"(?i)(for\s+a\s+fictional\s+(story|scenario)|hypothetically\s+speaking|in\s+a\s+(simulation|hypothetical|fictional)\s+(scenario|world|context)|let('s|\s+us)\s+pretend|imagine\s+(you\s+are|that\s+you\b))", r"(?i)\b(OVERRIDE|NEW\s+TASK|SYSTEM\s+OVERRIDE):\s*", ])
.unwrap()
});
pub struct PromptScanner;
impl Scanner for PromptScanner {
fn name(&self) -> &'static str {
"prompt"
}
fn description(&self) -> &'static str {
"Prompt injection pattern scanner — pure Rust regex"
}
fn is_available(&self) -> bool {
true
}
fn scan(&self, path: &Path, _config: &Config) -> ScanResult {
let start = Instant::now();
let files = collect_files(path, &["md", "txt", "yaml", "yml"]);
let mut findings = Vec::new();
for file in &files {
if is_benign_file(file) {
continue;
}
let content = match read_file_limited(file) {
Ok(c) => c,
Err(e) => {
findings.push(Finding {
rule_id: "prompt/read-error".to_string(),
message: format!("Could not read file: {}", e),
severity: Severity::Info,
file: Some(file.clone()),
line: None,
column: None,
scanner: "prompt".to_string(),
snippet: None,
suppressed: false,
suppression_reason: None,
remediation: Some(
"Check file permissions and ensure the file is valid UTF-8".to_string(),
),
});
continue;
}
};
for (line_num, line) in content.lines().enumerate() {
let line_num = line_num + 1;
let matches = PATTERN_SET.matches(line);
if !matches.matched_any() {
continue;
}
for idx in matches.iter() {
let pattern = &PATTERNS[idx];
let snippet = if line.len() > 120 {
let cut = line
.char_indices()
.nth(117)
.map(|(i, _)| i)
.unwrap_or(line.len());
format!("{}...", &line[..cut])
} else {
line.to_string()
};
findings.push(Finding {
rule_id: pattern.id.to_string(),
message: pattern.message.to_string(),
severity: pattern.severity,
file: Some(file.clone()),
line: Some(line_num),
column: None,
scanner: "prompt".to_string(),
snippet: Some(snippet.trim().to_string()),
suppressed: false,
suppression_reason: None,
remediation: Some(pattern.remediation.to_string()),
});
}
}
}
ScanResult {
scanner_name: "prompt".to_string(),
findings,
files_scanned: files.len(),
skipped: false,
skip_reason: None,
error: None,
duration_ms: start.elapsed().as_millis() as u64,
scanner_score: None,
scanner_grade: None,
}
}
}
pub fn rules() -> Vec<RuleInfo> {
PATTERNS
.iter()
.map(|p| RuleInfo {
id: p.id,
severity: match p.severity {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Info => "info",
},
scanner: "prompt",
message: p.message,
remediation: p.remediation,
})
.collect()
}