use super::{Found, REMOTE_TEXT_PROGRAMS, host_tokens, probe, program_name};
use crate::action::AtomicAction;
use crate::engine::CompiledPolicy;
use crate::injection::{self, InjectedLookup, Token, TokenKind};
const WEB_PROGRAMS: [&str; 7] = [
"invoke-webrequest",
"invoke-restmethod",
"iwr",
"irm",
"curl",
"wget",
"start-bitstransfer",
];
const REMOTE_PARAMETERS: [&str; 5] = ["-uri", "-computername", "-smtpserver", "-source", "-proxy"];
pub(super) struct Named {
pub(super) hosts: Vec<String>,
pub(super) remote: Vec<String>,
pub(super) paths: Vec<String>,
}
pub(super) fn named(command: &str) -> Named {
let unescaped = command.replace('`', "");
let mut out = Named {
hosts: Vec::new(),
remote: Vec::new(),
paths: Vec::new(),
};
for text in [command, unescaped.as_str()] {
let text = parameter_values(text);
let found = injection::candidates(&text);
let remote = found.url_hosts.iter().cloned().chain(remote_hosts(&text));
push_new(&mut out.remote, remote);
push_new(
&mut out.hosts,
found.url_hosts.into_iter().chain(found.word_hosts),
);
push_new(&mut out.paths, found.paths);
}
out
}
fn push_new(list: &mut Vec<String>, items: impl IntoIterator<Item = String>) {
for item in items {
if !list.contains(&item) {
list.push(item);
}
}
}
fn parameter_values(text: &str) -> String {
text.split(' ')
.map(|word| match word.split_once(':') {
Some((name, value)) if name.len() > 1 && name.starts_with('-') => {
format!("{name} {value}")
}
_ => word.to_owned(),
})
.collect::<Vec<_>>()
.join(" ")
}
fn segments(text: &str) -> Vec<Vec<String>> {
text.split(['\n', ';', '|', '(', ')', '{', '}'])
.flat_map(|part| part.split("&&"))
.map(|segment| {
let mut words: Vec<String> = segment
.split_whitespace()
.map(|w| w.trim_matches(['\'', '"', ',']).to_owned())
.filter(|w| !w.is_empty())
.collect();
while words
.first()
.is_some_and(|w| w == "&" || w == "." || w == "$")
{
words.remove(0);
}
if let Some(first) = words.first_mut()
&& let Some(rest) = first.strip_prefix('&')
{
*first = rest.trim_matches(['\'', '"']).to_owned();
}
words
})
.filter(|words| !words.is_empty())
.collect()
}
fn program_of(word: &str) -> String {
let name = program_name(word).to_ascii_lowercase();
name.strip_suffix(".exe").unwrap_or(&name).to_owned()
}
fn remote_hosts(text: &str) -> Vec<String> {
let mut hosts = Vec::new();
for words in segments(text) {
let web = WEB_PROGRAMS.contains(&program_of(&words[0]).as_str());
for (i, word) in words.iter().enumerate().skip(1) {
let after_parameter = REMOTE_PARAMETERS
.iter()
.any(|p| words[i - 1].eq_ignore_ascii_case(p));
if (web && !word.starts_with('-')) || after_parameter {
hosts.extend(crate::host::of_word(word));
}
}
}
hosts
}
impl CompiledPolicy<'_> {
pub(super) fn foreign_reads_remote(&self, command: &str) -> bool {
let forge = segments(&command.replace('`', ""))
.iter()
.any(|words| REMOTE_TEXT_PROGRAMS.contains(&program_of(&words[0]).as_str()));
forge
|| named(command).remote.into_iter().any(|host| {
!crate::host::is_loopback(&host) && !self.allows(&AtomicAction::Net { host })
})
}
pub(super) fn foreign_injected(&self, command: &str, seen: &dyn InjectedLookup) -> Vec<Found> {
let named = named(command);
let hosts = named.hosts.into_iter().filter_map(|host| {
let kinds = [TokenKind::UrlHost, TokenKind::WordHost];
let atom = AtomicAction::Net { host: host.clone() };
let (token, source) = probe(seen, host_tokens(&host, &kinds))
.or_else(|| self.parent_source(&atom, &host, seen))?;
(!self.allows(&atom)).then(|| Found {
part: format!("the host `{}`", token.text),
source,
})
});
let paths = named.paths.into_iter().filter_map(|raw| {
let ctx = &self.ctx;
let path = crate::paths::resolve(&raw, &ctx.home, ctx.project.as_deref(), None)?;
if self.in_project(&path) {
return None;
}
let token = Token {
kind: TokenKind::Path,
text: self.path_key(&path),
};
let (token, source) = probe(seen, [token])?;
(!self.allows(&AtomicAction::FsWrite { path })).then(|| Found {
part: format!("the path `{}`", token.text),
source,
})
});
hosts.chain(paths).collect()
}
}