Add YARA dependency and implement rule compilation
- Added yara crate v0.28 to ghost-core dependencies - Implemented real YARA rule compilation from .yar/.yara files - Added recursive rule file discovery in rules directory - Implemented memory scanning with compiled YARA rules - Added proper error handling for rule compilation and scanning - Cross-platform memory reading support (Windows, Linux, macOS stub) Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -15,6 +15,7 @@ serde_json = "1.0"
|
|||||||
uuid = { version = "1.0", features = ["v4"] }
|
uuid = { version = "1.0", features = ["v4"] }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
yara = "0.28"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "0.58", features = [
|
windows = { version = "0.58", features = [
|
||||||
|
|||||||
@@ -1,33 +1,28 @@
|
|||||||
use crate::{GhostError, MemoryRegion, ProcessInfo};
|
use crate::{GhostError, MemoryRegion, ProcessInfo};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
use yara::{Compiler, Rules, Scanner};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct DynamicYaraEngine {
|
pub struct DynamicYaraEngine {
|
||||||
rules: Vec<YaraRule>,
|
rules_path: Option<PathBuf>,
|
||||||
sources: Vec<YaraRuleSource>,
|
#[serde(skip)]
|
||||||
|
compiled_rules: Option<Rules>,
|
||||||
|
rule_metadata: Vec<YaraRuleMetadata>,
|
||||||
scan_cache: HashMap<String, CachedScanResult>,
|
scan_cache: HashMap<String, CachedScanResult>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct YaraRule {
|
pub struct YaraRuleMetadata {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub content: String,
|
pub file_path: String,
|
||||||
pub source: String,
|
|
||||||
pub threat_level: ThreatLevel,
|
pub threat_level: ThreatLevel,
|
||||||
pub last_updated: SystemTime,
|
pub last_updated: SystemTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct YaraRuleSource {
|
|
||||||
pub name: String,
|
|
||||||
pub url: String,
|
|
||||||
pub enabled: bool,
|
|
||||||
pub rule_count: usize,
|
|
||||||
pub last_update: SystemTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct YaraScanResult {
|
pub struct YaraScanResult {
|
||||||
pub matches: Vec<RuleMatch>,
|
pub matches: Vec<RuleMatch>,
|
||||||
@@ -38,13 +33,15 @@ pub struct YaraScanResult {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RuleMatch {
|
pub struct RuleMatch {
|
||||||
pub rule_name: String,
|
pub rule_name: String,
|
||||||
|
pub namespace: String,
|
||||||
pub threat_level: ThreatLevel,
|
pub threat_level: ThreatLevel,
|
||||||
pub offset: u64,
|
pub offset: u64,
|
||||||
pub length: u32,
|
pub length: u32,
|
||||||
pub metadata: HashMap<String, String>,
|
pub metadata: HashMap<String, String>,
|
||||||
|
pub matched_strings: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum ThreatLevel {
|
pub enum ThreatLevel {
|
||||||
Info = 1,
|
Info = 1,
|
||||||
Low = 2,
|
Low = 2,
|
||||||
@@ -53,6 +50,22 @@ pub enum ThreatLevel {
|
|||||||
Critical = 5,
|
Critical = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ThreatLevel {
|
||||||
|
pub fn from_metadata(metadata: &HashMap<String, String>) -> Self {
|
||||||
|
metadata
|
||||||
|
.get("threat_level")
|
||||||
|
.and_then(|s| match s.to_lowercase().as_str() {
|
||||||
|
"info" => Some(ThreatLevel::Info),
|
||||||
|
"low" => Some(ThreatLevel::Low),
|
||||||
|
"medium" => Some(ThreatLevel::Medium),
|
||||||
|
"high" => Some(ThreatLevel::High),
|
||||||
|
"critical" => Some(ThreatLevel::Critical),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(ThreatLevel::Medium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct CachedScanResult {
|
struct CachedScanResult {
|
||||||
result: YaraScanResult,
|
result: YaraScanResult,
|
||||||
@@ -60,96 +73,334 @@ struct CachedScanResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DynamicYaraEngine {
|
impl DynamicYaraEngine {
|
||||||
pub fn new(_config_path: Option<&str>) -> Result<Self, GhostError> {
|
/// Create a new YARA engine with optional custom rules directory
|
||||||
let sources = vec![
|
pub fn new(rules_path: Option<&str>) -> Result<Self, GhostError> {
|
||||||
YaraRuleSource {
|
let rules_path = rules_path.map(PathBuf::from);
|
||||||
name: "Malware Bazaar".to_string(),
|
|
||||||
url: "https://bazaar.abuse.ch/browse/".to_string(),
|
|
||||||
enabled: true,
|
|
||||||
rule_count: 0,
|
|
||||||
last_update: SystemTime::now(),
|
|
||||||
},
|
|
||||||
YaraRuleSource {
|
|
||||||
name: "VX-Underground".to_string(),
|
|
||||||
url: "https://vx-underground.org/yara".to_string(),
|
|
||||||
enabled: true,
|
|
||||||
rule_count: 0,
|
|
||||||
last_update: SystemTime::now(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
Ok(DynamicYaraEngine {
|
let mut engine = DynamicYaraEngine {
|
||||||
rules: Vec::new(),
|
rules_path,
|
||||||
sources,
|
compiled_rules: None,
|
||||||
|
rule_metadata: Vec::new(),
|
||||||
scan_cache: HashMap::new(),
|
scan_cache: HashMap::new(),
|
||||||
})
|
};
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_rules(&mut self) -> Result<usize, GhostError> {
|
// Attempt to load rules if path is provided
|
||||||
let mut updated_count = 0;
|
if engine.rules_path.is_some() {
|
||||||
|
if let Err(e) = engine.compile_rules() {
|
||||||
for source in &mut self.sources {
|
log::warn!("Failed to compile YARA rules: {:?}", e);
|
||||||
if !source.enabled {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulate rule download
|
|
||||||
let new_rules = vec![YaraRule {
|
|
||||||
name: format!("generic_malware_{}", updated_count + 1),
|
|
||||||
content: "rule generic_malware { condition: true }".to_string(),
|
|
||||||
source: source.name.clone(),
|
|
||||||
threat_level: ThreatLevel::Medium,
|
|
||||||
last_updated: SystemTime::now(),
|
|
||||||
}];
|
|
||||||
|
|
||||||
self.rules.extend(new_rules);
|
|
||||||
source.rule_count = self.rules.len();
|
|
||||||
source.last_update = SystemTime::now();
|
|
||||||
updated_count += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(updated_count)
|
Ok(engine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compile all YARA rules from the rules directory
|
||||||
|
pub fn compile_rules(&mut self) -> Result<usize, GhostError> {
|
||||||
|
let rules_dir = self.rules_path.as_ref().ok_or_else(|| {
|
||||||
|
GhostError::ConfigurationError("No rules directory configured".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !rules_dir.exists() {
|
||||||
|
return Err(GhostError::ConfigurationError(format!(
|
||||||
|
"Rules directory does not exist: {}",
|
||||||
|
rules_dir.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut compiler = Compiler::new()
|
||||||
|
.map_err(|e| GhostError::ConfigurationError(format!("YARA compiler error: {}", e)))?;
|
||||||
|
|
||||||
|
let mut rule_count = 0;
|
||||||
|
self.rule_metadata.clear();
|
||||||
|
|
||||||
|
// Recursively find and compile all .yar and .yara files
|
||||||
|
let rule_files = Self::find_rule_files(rules_dir)?;
|
||||||
|
|
||||||
|
for rule_file in &rule_files {
|
||||||
|
match fs::read_to_string(rule_file) {
|
||||||
|
Ok(content) => {
|
||||||
|
let namespace = rule_file
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("default");
|
||||||
|
|
||||||
|
match compiler.add_rules_str_with_namespace(&content, namespace) {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!("Compiled YARA rule: {}", rule_file.display());
|
||||||
|
|
||||||
|
self.rule_metadata.push(YaraRuleMetadata {
|
||||||
|
name: namespace.to_string(),
|
||||||
|
file_path: rule_file.display().to_string(),
|
||||||
|
threat_level: ThreatLevel::Medium,
|
||||||
|
last_updated: SystemTime::now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
rule_count += 1;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to compile {}: {}", rule_file.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to read {}: {}", rule_file.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule_count == 0 {
|
||||||
|
return Err(GhostError::ConfigurationError(
|
||||||
|
"No YARA rules were successfully compiled".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.compiled_rules = Some(
|
||||||
|
compiler
|
||||||
|
.compile_rules()
|
||||||
|
.map_err(|e| GhostError::ConfigurationError(format!("Rule compilation failed: {}", e)))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
log::info!("Successfully compiled {} YARA rules", rule_count);
|
||||||
|
Ok(rule_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find all YARA rule files in the given directory
|
||||||
|
fn find_rule_files(dir: &Path) -> Result<Vec<PathBuf>, GhostError> {
|
||||||
|
let mut rule_files = Vec::new();
|
||||||
|
|
||||||
|
if !dir.is_dir() {
|
||||||
|
return Ok(rule_files);
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = fs::read_dir(dir).map_err(|e| {
|
||||||
|
GhostError::ConfigurationError(format!("Failed to read rules directory: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.is_file() {
|
||||||
|
if let Some(ext) = path.extension() {
|
||||||
|
if ext == "yar" || ext == "yara" {
|
||||||
|
rule_files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if path.is_dir() {
|
||||||
|
// Recursively search subdirectories
|
||||||
|
rule_files.extend(Self::find_rule_files(&path)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(rule_files)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan process memory regions with compiled YARA rules
|
||||||
pub async fn scan_process(
|
pub async fn scan_process(
|
||||||
&self,
|
&self,
|
||||||
_process: &ProcessInfo,
|
process: &ProcessInfo,
|
||||||
memory_regions: &[MemoryRegion],
|
memory_regions: &[MemoryRegion],
|
||||||
) -> Result<YaraScanResult, GhostError> {
|
) -> Result<YaraScanResult, GhostError> {
|
||||||
let start_time = SystemTime::now();
|
let start_time = SystemTime::now();
|
||||||
let mut matches = Vec::new();
|
|
||||||
let mut bytes_scanned = 0;
|
|
||||||
|
|
||||||
// Simulate YARA scanning
|
let rules = self.compiled_rules.as_ref().ok_or_else(|| {
|
||||||
|
GhostError::ConfigurationError("YARA rules not compiled".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut all_matches = Vec::new();
|
||||||
|
let mut bytes_scanned = 0u64;
|
||||||
|
|
||||||
|
// Scan each executable memory region
|
||||||
for region in memory_regions.iter() {
|
for region in memory_regions.iter() {
|
||||||
bytes_scanned += region.size;
|
// Only scan executable regions with reasonable size
|
||||||
|
if !region.protection.is_executable() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Simulate finding suspicious patterns
|
if region.size > 100 * 1024 * 1024 {
|
||||||
if region.protection.is_executable() && region.protection.is_writable() {
|
log::debug!(
|
||||||
matches.push(RuleMatch {
|
"Skipping large region at {:#x} (size: {} MB)",
|
||||||
rule_name: "suspicious_rwx_memory".to_string(),
|
region.base_address,
|
||||||
threat_level: ThreatLevel::High,
|
region.size / (1024 * 1024)
|
||||||
offset: region.base_address as u64,
|
);
|
||||||
length: 1024,
|
continue;
|
||||||
metadata: HashMap::new(),
|
}
|
||||||
});
|
|
||||||
|
// Read memory content
|
||||||
|
let memory_content = match Self::read_process_memory(process.pid, region) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!(
|
||||||
|
"Failed to read memory at {:#x}: {:?}",
|
||||||
|
region.base_address,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bytes_scanned += memory_content.len() as u64;
|
||||||
|
|
||||||
|
// Perform YARA scan on this memory region
|
||||||
|
match Self::scan_memory_with_yara(rules, &memory_content, region.base_address) {
|
||||||
|
Ok(mut matches) => {
|
||||||
|
all_matches.append(&mut matches);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("YARA scan error at {:#x}: {:?}", region.base_address, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let scan_time_ms = start_time.elapsed().unwrap_or_default().as_millis() as u64;
|
let scan_time_ms = start_time.elapsed().unwrap_or_default().as_millis() as u64;
|
||||||
|
|
||||||
Ok(YaraScanResult {
|
Ok(YaraScanResult {
|
||||||
matches,
|
matches: all_matches,
|
||||||
scan_time_ms,
|
scan_time_ms,
|
||||||
bytes_scanned: bytes_scanned as u64,
|
bytes_scanned,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rule_count(&self) -> usize {
|
/// Scan a memory buffer with YARA rules
|
||||||
self.rules.len()
|
fn scan_memory_with_yara(
|
||||||
|
rules: &Rules,
|
||||||
|
data: &[u8],
|
||||||
|
base_address: usize,
|
||||||
|
) -> Result<Vec<RuleMatch>, GhostError> {
|
||||||
|
let mut scanner = Scanner::new(rules)
|
||||||
|
.map_err(|e| GhostError::ScanError(format!("Scanner creation failed: {}", e)))?;
|
||||||
|
|
||||||
|
let scan_results = scanner
|
||||||
|
.scan_mem(data)
|
||||||
|
.map_err(|e| GhostError::ScanError(format!("Scan failed: {}", e)))?;
|
||||||
|
|
||||||
|
let mut matches = Vec::new();
|
||||||
|
|
||||||
|
for rule in scan_results {
|
||||||
|
let rule_name = rule.identifier.to_string();
|
||||||
|
let namespace = rule.namespace.to_string();
|
||||||
|
|
||||||
|
// Extract metadata
|
||||||
|
let mut metadata = HashMap::new();
|
||||||
|
for meta in rule.metadatas {
|
||||||
|
let value = match meta.value {
|
||||||
|
yara::MetadataValue::Integer(i) => i.to_string(),
|
||||||
|
yara::MetadataValue::String(ref s) => s.clone(),
|
||||||
|
yara::MetadataValue::Boolean(b) => b.to_string(),
|
||||||
|
};
|
||||||
|
metadata.insert(meta.identifier.to_string(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let threat_level = ThreatLevel::from_metadata(&metadata);
|
||||||
|
|
||||||
|
// Extract matched strings
|
||||||
|
let mut matched_strings = Vec::new();
|
||||||
|
for string in rule.strings {
|
||||||
|
let identifier = string.identifier.to_string();
|
||||||
|
for m in string.matches {
|
||||||
|
matched_strings.push(format!(
|
||||||
|
"{} at offset {:#x}",
|
||||||
|
identifier,
|
||||||
|
base_address + m.offset
|
||||||
|
));
|
||||||
|
|
||||||
|
// Create a match entry for each string match
|
||||||
|
matches.push(RuleMatch {
|
||||||
|
rule_name: rule_name.clone(),
|
||||||
|
namespace: namespace.clone(),
|
||||||
|
threat_level,
|
||||||
|
offset: (base_address + m.offset) as u64,
|
||||||
|
length: m.length as u32,
|
||||||
|
metadata: metadata.clone(),
|
||||||
|
matched_strings: vec![identifier.clone()],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(matches)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_sources(&self) -> &[YaraRuleSource] {
|
/// Read memory from a specific process and region
|
||||||
&self.sources
|
#[cfg(target_os = "windows")]
|
||||||
|
fn read_process_memory(
|
||||||
|
pid: u32,
|
||||||
|
region: &MemoryRegion,
|
||||||
|
) -> Result<Vec<u8>, GhostError> {
|
||||||
|
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
||||||
|
use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory;
|
||||||
|
use windows::Win32::System::Threading::{OpenProcess, PROCESS_VM_READ};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let handle = OpenProcess(PROCESS_VM_READ, false, pid)
|
||||||
|
.map_err(|e| GhostError::MemoryReadError(format!("OpenProcess failed: {}", e)))?;
|
||||||
|
|
||||||
|
let mut buffer = vec![0u8; region.size];
|
||||||
|
let mut bytes_read = 0;
|
||||||
|
|
||||||
|
let result = ReadProcessMemory(
|
||||||
|
handle,
|
||||||
|
region.base_address as *const _,
|
||||||
|
buffer.as_mut_ptr() as *mut _,
|
||||||
|
region.size,
|
||||||
|
Some(&mut bytes_read),
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = CloseHandle(handle);
|
||||||
|
|
||||||
|
if result.is_ok() && bytes_read > 0 {
|
||||||
|
buffer.truncate(bytes_read);
|
||||||
|
Ok(buffer)
|
||||||
|
} else {
|
||||||
|
Err(GhostError::MemoryReadError(
|
||||||
|
"ReadProcessMemory failed".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read memory from a specific process and region (Linux implementation)
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn read_process_memory(
|
||||||
|
pid: u32,
|
||||||
|
region: &MemoryRegion,
|
||||||
|
) -> Result<Vec<u8>, GhostError> {
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{Read, Seek, SeekFrom};
|
||||||
|
|
||||||
|
let mem_path = format!("/proc/{}/mem", pid);
|
||||||
|
let mut file = File::open(&mem_path)
|
||||||
|
.map_err(|e| GhostError::MemoryReadError(format!("Failed to open {}: {}", mem_path, e)))?;
|
||||||
|
|
||||||
|
file.seek(SeekFrom::Start(region.base_address as u64))
|
||||||
|
.map_err(|e| GhostError::MemoryReadError(format!("Seek failed: {}", e)))?;
|
||||||
|
|
||||||
|
let mut buffer = vec![0u8; region.size];
|
||||||
|
file.read_exact(&mut buffer)
|
||||||
|
.map_err(|e| GhostError::MemoryReadError(format!("Read failed: {}", e)))?;
|
||||||
|
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read memory from a specific process and region (macOS implementation)
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn read_process_memory(
|
||||||
|
_pid: u32,
|
||||||
|
_region: &MemoryRegion,
|
||||||
|
) -> Result<Vec<u8>, GhostError> {
|
||||||
|
Err(GhostError::NotImplemented(
|
||||||
|
"Memory reading not implemented for macOS".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_rule_count(&self) -> usize {
|
||||||
|
self.rule_metadata.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_rule_metadata(&self) -> &[YaraRuleMetadata] {
|
||||||
|
&self.rule_metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_compiled(&self) -> bool {
|
||||||
|
self.compiled_rules.is_some()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user