use tower_lsp_server::ls_types::*;
use crate::data;
pub fn complete(text: &str, position: Position) -> Vec<CompletionItem> {
let lines: Vec<&str> = text.lines().collect();
let line_idx = position.line as usize;
if line_idx >= lines.len() {
return vec![];
}
let line = lines[line_idx];
let col = (position.character as usize).min(line.len());
let col = if line.is_char_boundary(col) {
col
} else {
(0..col)
.rev()
.find(|&i| line.is_char_boundary(i))
.unwrap_or(0)
};
let prefix = &line[..col];
let trimmed = prefix.trim_start();
let indent = prefix.len() - trimmed.len();
if let Some(pos) = trimmed.rfind('|') {
let after_pipe = &trimmed[pos + 1..];
return modifier_completions(after_pipe);
}
if is_in_section(&lines, line_idx, "tags") && trimmed.starts_with("- ") {
let tag_prefix = trimmed.strip_prefix("- ").unwrap_or("");
return tag_completions(tag_prefix);
}
if trimmed.starts_with("condition:") || is_in_section(&lines, line_idx, "condition") {
let cond_text = trimmed
.strip_prefix("condition:")
.unwrap_or(trimmed)
.trim_start();
return condition_completions(text, cond_text);
}
if let Some(colon_pos) = trimmed.find(':') {
let key = trimmed[..colon_pos].trim();
let value_prefix = trimmed[colon_pos + 1..].trim_start();
if let Some(items) = value_completions(key, value_prefix) {
return items;
}
}
if indent == 0 && !trimmed.contains(':') {
return top_level_key_completions(trimmed);
}
if indent > 0 && is_in_section(&lines, line_idx, "logsource") && !trimmed.contains(':') {
return logsource_key_completions(trimmed);
}
if indent > 0 && is_in_section(&lines, line_idx, "detection") && !trimmed.contains(':') {
return detection_key_completions(trimmed);
}
vec![]
}
fn is_in_section(lines: &[&str], current_line: usize, section: &str) -> bool {
let pattern = format!("{section}:");
for line in lines[..current_line].iter().rev() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let indent = line.len() - trimmed.len();
if indent == 0 && trimmed.ends_with(':') {
return trimmed == pattern;
}
if indent == 0 && trimmed.contains(':') {
let key = trimmed.split(':').next().unwrap_or("");
return key == section;
}
}
false
}
fn modifier_completions(prefix: &str) -> Vec<CompletionItem> {
data::MODIFIERS
.iter()
.filter(|(name, _)| name.starts_with(prefix))
.map(|(name, doc)| CompletionItem {
label: name.to_string(),
kind: Some(CompletionItemKind::ENUM_MEMBER),
detail: Some(doc.to_string()),
insert_text: Some(name.to_string()),
..Default::default()
})
.collect()
}
fn tag_completions(prefix: &str) -> Vec<CompletionItem> {
const EXTRA_TAGS: &[(&str, &str)] = &[
("cve.", "CVE identifier (e.g. cve.2024.1234)"),
("detection.dfir", "DFIR detection"),
("detection.emerging_threats", "Emerging threats detection"),
("detection.threat_hunting", "Threat hunting detection"),
("tlp.white", "TLP:WHITE \u{2014} Unlimited disclosure"),
("tlp.green", "TLP:GREEN \u{2014} Community-wide"),
("tlp.amber", "TLP:AMBER \u{2014} Limited disclosure"),
("tlp.red", "TLP:RED \u{2014} Named recipients only"),
];
data::MITRE_TACTICS
.iter()
.chain(EXTRA_TAGS.iter())
.filter(|(tag, _)| tag.starts_with(prefix))
.map(|(tag, doc)| CompletionItem {
label: tag.to_string(),
kind: Some(CompletionItemKind::VALUE),
detail: Some(doc.to_string()),
..Default::default()
})
.collect()
}
fn condition_completions(text: &str, prefix: &str) -> Vec<CompletionItem> {
let mut items = Vec::new();
let selection_names = extract_selection_names(text);
for name in &selection_names {
if name.starts_with(prefix) || prefix.is_empty() {
items.push(CompletionItem {
label: name.clone(),
kind: Some(CompletionItemKind::VARIABLE),
detail: Some("Detection selection".to_string()),
..Default::default()
});
}
}
let keywords: &[(&str, &str)] = &[
("and", "Boolean AND"),
("or", "Boolean OR"),
("not", "Boolean NOT"),
("1 of", "At least one of the matching selections"),
("all of", "All matching selections"),
("1 of them", "At least one of all selections"),
("all of them", "All selections must match"),
];
for (kw, doc) in keywords {
if kw.starts_with(prefix) || prefix.is_empty() {
items.push(CompletionItem {
label: kw.to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some(doc.to_string()),
..Default::default()
});
}
}
items
}
fn extract_selection_names(text: &str) -> Vec<String> {
let mut names = Vec::new();
let mut in_detection = false;
let mut detection_indent: Option<usize> = None;
for line in text.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let indent = line.len() - trimmed.len();
if indent == 0 && (trimmed == "detection:" || trimmed.starts_with("detection:")) {
in_detection = true;
detection_indent = None;
continue;
}
if indent == 0 && trimmed.contains(':') && in_detection {
in_detection = false;
continue;
}
if in_detection {
if detection_indent.is_none() && trimmed.ends_with(':') {
detection_indent = Some(indent);
}
if let Some(det_indent) = detection_indent
&& indent == det_indent
&& trimmed.ends_with(':')
{
let key = &trimmed[..trimmed.len() - 1];
if key != "condition" {
names.push(key.to_string());
}
}
}
}
names
}
fn value_completions(key: &str, prefix: &str) -> Option<Vec<CompletionItem>> {
let values: &[(&str, &str)] = match key {
"status" => &[
("stable", "Confirmed and widely tested"),
("test", "Under testing, may have FPs"),
("experimental", "New rule, expect FPs"),
("deprecated", "No longer maintained"),
("unsupported", "Cannot be used as-is"),
],
"level" => &[
("informational", "No threat, just informational"),
("low", "Rarely interesting"),
("medium", "Might warrant investigation"),
("high", "Likely malicious activity"),
("critical", "Almost certainly malicious"),
],
"product" => &[
("windows", "Microsoft Windows"),
("linux", "Linux"),
("macos", "Apple macOS"),
("aws", "Amazon Web Services"),
("azure", "Microsoft Azure"),
("gcp", "Google Cloud Platform"),
("m365", "Microsoft 365"),
("okta", "Okta Identity"),
("github", "GitHub"),
],
"category" => &[
("process_creation", "Process creation events"),
("file_event", "File system events"),
("file_change", "File change/modification events"),
("file_rename", "File rename events"),
("file_delete", "File deletion events"),
("file_access", "File access events"),
("registry_event", "Windows Registry events"),
("registry_set", "Registry value set"),
("registry_add", "Registry key creation"),
("registry_delete", "Registry key/value deletion"),
("network_connection", "Network connections"),
("dns_query", "DNS queries"),
("image_load", "DLL/image load events"),
("driver_load", "Driver load events"),
("pipe_created", "Named pipe creation"),
("ps_script", "PowerShell script execution"),
("ps_module", "PowerShell module logging"),
("ps_classic_start", "PowerShell classic start"),
("wmi_event", "WMI event subscription"),
("create_remote_thread", "Remote thread creation"),
("create_stream_hash", "Alternate data stream"),
("sysmon_error", "Sysmon operational errors"),
("sysmon_status", "Sysmon status events"),
("clipboard_capture", "Clipboard capture"),
("firewall", "Firewall events"),
("webserver", "Web server logs"),
("proxy", "Proxy logs"),
("antivirus", "Antivirus detections"),
],
"service" => &[
("sysmon", "Sysmon"),
("security", "Windows Security"),
("system", "Windows System"),
("application", "Windows Application"),
("powershell", "PowerShell"),
("powershell-classic", "PowerShell Classic"),
("windefend", "Windows Defender"),
("applocker", "AppLocker"),
("firewall-as", "Windows Firewall"),
("bits-client", "BITS Client"),
("codeintegrity-operational", "Code Integrity"),
("dns-server", "DNS Server"),
("driver-framework", "Driver Framework"),
("msexchange-management", "Exchange Management"),
("ntlm", "NTLM Authentication"),
("openssh", "OpenSSH"),
("printservice-admin", "Print Service Admin"),
("printservice-operational", "Print Service Operational"),
("smbclient-security", "SMB Client Security"),
("taskscheduler", "Task Scheduler"),
(
"terminalservices-localsessionmanager",
"Terminal Services LSM",
),
("wmi", "WMI"),
],
_ => return None,
};
Some(
values
.iter()
.filter(|(val, _)| val.starts_with(prefix) || prefix.is_empty())
.map(|(val, doc)| CompletionItem {
label: val.to_string(),
kind: Some(CompletionItemKind::ENUM_MEMBER),
detail: Some(doc.to_string()),
..Default::default()
})
.collect(),
)
}
fn top_level_key_completions(prefix: &str) -> Vec<CompletionItem> {
let keys: &[(&str, &str, &str)] = &[
("title", "title: $0", "Rule title (required)"),
("id", "id: $0", "Unique UUID identifier"),
(
"related",
"related:\n - id: $1\n type: $0",
"Related rules",
),
("status", "status: $0", "Rule maturity status"),
("description", "description: $0", "Detailed description"),
("references", "references:\n - $0", "External references"),
("author", "author: $0", "Rule author"),
("date", "date: $0", "Creation date (YYYY-MM-DD)"),
(
"modified",
"modified: $0",
"Last modified date (YYYY-MM-DD)",
),
("tags", "tags:\n - $0", "Classification tags"),
(
"logsource",
"logsource:\n category: $1\n product: $0",
"Log source definition",
),
(
"detection",
"detection:\n ${1:selection}:\n ${2:FieldName}: ${3:value}\n condition: ${1:selection}$0",
"Detection logic",
),
(
"falsepositives",
"falsepositives:\n - $0",
"Known false positives",
),
("level", "level: $0", "Severity level"),
(
"fields",
"fields:\n - $0",
"Interesting fields to extract",
),
(
"correlation",
"correlation:\n type: $1\n rules:\n - $2\n group-by:\n - $3\n timespan: $4\n condition:\n gte: $0",
"Correlation rule",
),
(
"filter",
"filter:\n rules:\n - $1\n ${2:selection}:\n ${3:FieldName}: ${4:value}\n condition: ${2:selection}$0",
"Filter rule",
),
];
keys.iter()
.filter(|(key, _, _)| key.starts_with(prefix) || prefix.is_empty())
.map(|(key, snippet, doc)| CompletionItem {
label: key.to_string(),
kind: Some(CompletionItemKind::PROPERTY),
detail: Some(doc.to_string()),
insert_text: Some(snippet.to_string()),
insert_text_format: Some(InsertTextFormat::SNIPPET),
..Default::default()
})
.collect()
}
fn logsource_key_completions(prefix: &str) -> Vec<CompletionItem> {
let keys: &[(&str, &str)] = &[
("category", "Log category (e.g. process_creation)"),
("product", "Log product (e.g. windows)"),
("service", "Log service (e.g. sysmon)"),
("definition", "Logsource definition note"),
];
keys.iter()
.filter(|(key, _)| key.starts_with(prefix) || prefix.is_empty())
.map(|(key, doc)| CompletionItem {
label: format!("{key}: "),
kind: Some(CompletionItemKind::PROPERTY),
detail: Some(doc.to_string()),
..Default::default()
})
.collect()
}
fn detection_key_completions(prefix: &str) -> Vec<CompletionItem> {
let keys: &[(&str, &str)] = &[
("selection", "Detection selection block"),
("filter", "Filter block"),
("condition", "Boolean condition expression"),
];
keys.iter()
.filter(|(key, _)| key.starts_with(prefix) || prefix.is_empty())
.map(|(key, doc)| CompletionItem {
label: format!("{key}: "),
kind: Some(CompletionItemKind::PROPERTY),
detail: Some(doc.to_string()),
..Default::default()
})
.collect()
}