use perl_diagnostics_codes::DiagnosticCode;
use perl_parser_core::ast::{Node, NodeKind};
use super::super::walker::walk_node;
use perl_lsp_diagnostic_types::{Diagnostic, DiagnosticSeverity, RelatedInformation};
pub fn check_security(node: &Node, diagnostics: &mut Vec<Diagnostic>) {
walk_node(node, &mut |n| {
match &n.kind {
NodeKind::FunctionCall { name, args } => {
check_two_arg_open(name, args, n, diagnostics);
check_string_eval(name, args, n, diagnostics);
}
NodeKind::Eval { block } => {
check_eval_node(block, n, diagnostics);
}
NodeKind::String { value, interpolated: true } if is_backtick_string(value) => {
diagnostics.push(Diagnostic {
range: (n.location.start, n.location.end),
severity: DiagnosticSeverity::Information,
code: Some(DiagnosticCode::SecurityBacktickExec.as_str().to_string()),
message: "Command execution detected. Ensure input is sanitized.".to_string(),
related_information: vec![RelatedInformation {
location: (n.location.start, n.location.end),
message: "Consider using open() with a pipe, or IPC::Run for safer command execution with proper input validation".to_string(),
}],
tags: Vec::new(),
suggestion: Some(
"Use open(my $fh, '-|', @cmd) or IPC::Run for safer command execution"
.to_string(),
),
});
}
_ => {}
}
});
}
fn check_eval_node(block: &Node, eval_node: &Node, diagnostics: &mut Vec<Diagnostic>) {
let is_string_eval = matches!(&block.kind, NodeKind::String { .. } | NodeKind::Variable { .. })
|| matches!(&block.kind, NodeKind::Binary { op, .. } if op == ".");
if !is_string_eval {
return;
}
diagnostics.push(Diagnostic {
range: (eval_node.location.start, eval_node.location.end),
severity: DiagnosticSeverity::Warning,
code: Some(DiagnosticCode::SecurityStringEval.as_str().to_string()),
message: "String eval is a security risk. Consider eval { } for exception handling."
.to_string(),
related_information: vec![RelatedInformation {
location: (eval_node.location.start, eval_node.location.end),
message: "String eval executes arbitrary Perl code at runtime. If the string contains user input, this allows code injection.".to_string(),
}],
tags: Vec::new(),
suggestion: Some(
"Use eval { } for exception handling, or consider safer alternatives like Try::Tiny"
.to_string(),
),
});
}
fn check_two_arg_open(name: &str, args: &[Node], node: &Node, diagnostics: &mut Vec<Diagnostic>) {
if name != "open" || args.len() != 2 {
return;
}
diagnostics.push(Diagnostic {
range: (node.location.start, node.location.end),
severity: DiagnosticSeverity::Warning,
code: Some(DiagnosticCode::TwoArgOpen.as_str().to_string()),
message: "Use 3-argument open for safety: open(my $fh, '>', 'file')".to_string(),
related_information: vec![RelatedInformation {
location: (node.location.start, node.location.end),
message: "Two-argument open combines mode and filename, which can allow shell injection if the filename is derived from user input".to_string(),
}],
tags: Vec::new(),
suggestion: Some("Replace with 3-arg form: open(my $fh, '>', $file)".to_string()),
});
}
fn check_string_eval(name: &str, args: &[Node], node: &Node, diagnostics: &mut Vec<Diagnostic>) {
if name != "eval" {
return;
}
let is_string_arg = args.first().is_some_and(|arg| match &arg.kind {
NodeKind::String { .. } | NodeKind::Variable { .. } => true,
NodeKind::Binary { op, .. } if op == "." => true,
_ => false,
});
if !is_string_arg && !args.is_empty() {
return;
}
diagnostics.push(Diagnostic {
range: (node.location.start, node.location.end),
severity: DiagnosticSeverity::Warning,
code: Some(DiagnosticCode::SecurityStringEval.as_str().to_string()),
message: "String eval is a security risk. Consider eval { } for exception handling."
.to_string(),
related_information: vec![RelatedInformation {
location: (node.location.start, node.location.end),
message: "String eval executes arbitrary Perl code at runtime. If the string contains user input, this allows code injection.".to_string(),
}],
tags: Vec::new(),
suggestion: Some(
"Use eval { } for exception handling, or consider safer alternatives like Try::Tiny"
.to_string(),
),
});
}
fn is_backtick_string(value: &str) -> bool {
value.starts_with('`') && value.ends_with('`') && value.len() >= 2
}