mirror of
https://github.com/usestrix/firewall.git
synced 2026-11-09 05:19:35 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01ab5b0995 | ||
|
|
97a5a148e0 | ||
|
|
877807740b | ||
|
|
cd9b0614bc | ||
|
|
6cad09d32d |
No files matched your search
@@ -11,4 +11,5 @@ pub static BUN: Manager = Manager {
|
|||||||
("npm_config_registry", "http://registry.npmjs.org/"),
|
("npm_config_registry", "http://registry.npmjs.org/"),
|
||||||
],
|
],
|
||||||
args: &[],
|
args: &[],
|
||||||
|
lockfile: None,
|
||||||
};
|
};
|
||||||
@@ -17,4 +17,5 @@ pub static CARGO: Manager = Manager {
|
|||||||
"source.firewall.registry=\"sparse+http://index.crates.io/\"",
|
"source.firewall.registry=\"sparse+http://index.crates.io/\"",
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
|
lockfile: None,
|
||||||
};
|
};
|
||||||
@@ -8,4 +8,5 @@ pub static GEM: Manager = Manager {
|
|||||||
when: &["install", "update", "fetch"],
|
when: &["install", "update", "fetch"],
|
||||||
flags: &["--clear-sources", "--source", "http://rubygems.org/"],
|
flags: &["--clear-sources", "--source", "http://rubygems.org/"],
|
||||||
}],
|
}],
|
||||||
|
lockfile: None,
|
||||||
};
|
};
|
||||||
@@ -11,4 +11,5 @@ pub static GO: Manager = Manager {
|
|||||||
("GOFLAGS", "-mod=mod"),
|
("GOFLAGS", "-mod=mod"),
|
||||||
],
|
],
|
||||||
args: &[],
|
args: &[],
|
||||||
|
lockfile: None,
|
||||||
};
|
};
|
||||||
@@ -36,6 +36,14 @@ pub struct Manager {
|
|||||||
pub command: &'static str,
|
pub command: &'static str,
|
||||||
pub env: &'static [(&'static str, &'static str)],
|
pub env: &'static [(&'static str, &'static str)],
|
||||||
pub args: &'static [Args],
|
pub args: &'static [Args],
|
||||||
|
/// Where the manager records registry URLs, which then carry our http:// override.
|
||||||
|
pub lockfile: Option<Lockfile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What to do after a run about http:// registry URLs left in the lockfile at this path.
|
||||||
|
pub enum Lockfile {
|
||||||
|
Restore(&'static str),
|
||||||
|
Warn(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static MANAGERS: &[&Manager] =
|
pub static MANAGERS: &[&Manager] =
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::{Manager, PROXY};
|
use super::{Lockfile, Manager, PROXY};
|
||||||
|
|
||||||
pub static NPM: Manager = Manager {
|
pub static NPM: Manager = Manager {
|
||||||
command: "npm",
|
command: "npm",
|
||||||
@@ -8,4 +8,5 @@ pub static NPM: Manager = Manager {
|
|||||||
("npm_config_registry", "http://registry.npmjs.org/"),
|
("npm_config_registry", "http://registry.npmjs.org/"),
|
||||||
],
|
],
|
||||||
args: &[],
|
args: &[],
|
||||||
|
lockfile: Some(Lockfile::Restore("package-lock.json")),
|
||||||
};
|
};
|
||||||
@@ -13,4 +13,5 @@ pub static PDM: Manager = Manager {
|
|||||||
("PDM_PYPI_VERIFY_SSL", "false"),
|
("PDM_PYPI_VERIFY_SSL", "false"),
|
||||||
],
|
],
|
||||||
args: &[],
|
args: &[],
|
||||||
|
lockfile: None,
|
||||||
};
|
};
|
||||||
@@ -12,5 +12,5 @@ pub static PIP: Manager = recipe("pip");
|
|||||||
pub static PIP3: Manager = recipe("pip3");
|
pub static PIP3: Manager = recipe("pip3");
|
||||||
|
|
||||||
const fn recipe(name: &'static str) -> Manager {
|
const fn recipe(name: &'static str) -> Manager {
|
||||||
Manager { command: name, env: PIP_ENV, args: &[] }
|
Manager { command: name, env: PIP_ENV, args: &[], lockfile: None }
|
||||||
}
|
}
|
||||||
@@ -13,4 +13,5 @@ pub static PIPENV: Manager = Manager {
|
|||||||
("PIP_TRUSTED_HOST", "pypi.org files.pythonhosted.org"),
|
("PIP_TRUSTED_HOST", "pypi.org files.pythonhosted.org"),
|
||||||
],
|
],
|
||||||
args: &[],
|
args: &[],
|
||||||
|
lockfile: None,
|
||||||
};
|
};
|
||||||
@@ -9,4 +9,5 @@ pub static PNPM: Manager = Manager {
|
|||||||
("pnpm_config_registry", "http://registry.npmjs.org/"),
|
("pnpm_config_registry", "http://registry.npmjs.org/"),
|
||||||
],
|
],
|
||||||
args: &[],
|
args: &[],
|
||||||
|
lockfile: None,
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
use super::{Manager, PROXY, PYPI_SIMPLE};
|
use super::{Lockfile, Manager, PROXY, PYPI_SIMPLE};
|
||||||
|
|
||||||
pub static UV: Manager = Manager {
|
pub static UV: Manager = Manager {
|
||||||
command: "uv",
|
command: "uv",
|
||||||
env: &[("HTTP_PROXY", PROXY), ("http_proxy", PROXY), ("UV_DEFAULT_INDEX", PYPI_SIMPLE)],
|
env: &[("HTTP_PROXY", PROXY), ("http_proxy", PROXY), ("UV_DEFAULT_INDEX", PYPI_SIMPLE)],
|
||||||
args: &[],
|
args: &[],
|
||||||
|
lockfile: Some(Lockfile::Warn("uv.lock")),
|
||||||
};
|
};
|
||||||
@@ -6,9 +6,11 @@ mod windows;
|
|||||||
pub use forward::spawn;
|
pub use forward::spawn;
|
||||||
pub use managers::*;
|
pub use managers::*;
|
||||||
|
|
||||||
|
use crate::policies::registries::BUILT_IN;
|
||||||
use std::io::{Error, ErrorKind};
|
use std::io::{Error, ErrorKind};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
pub struct Invocation {
|
pub struct Invocation {
|
||||||
pub program: String,
|
pub program: String,
|
||||||
@@ -50,10 +52,42 @@ pub fn run(port: u16, invocation: &Invocation, dir: Option<&Path>) -> std::io::R
|
|||||||
let status = command.status();
|
let status = command.status();
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let status = status.map_err(|error| windows::explain(error, &program));
|
let status = status.map_err(|error| windows::explain(error, &program));
|
||||||
Ok(status?.code().unwrap_or(1))
|
let code = status?.code().unwrap_or(1);
|
||||||
|
let manager = MANAGERS.iter().find(|manager| manager.command == invocation.program);
|
||||||
|
match manager.and_then(|manager| manager.lockfile.as_ref()) {
|
||||||
|
Some(Lockfile::Restore(lockfile)) => restore_https(lockfile, dir),
|
||||||
|
Some(Lockfile::Warn(lockfile)) => warn_if_plaintext(lockfile, dir),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
Ok(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command::new only finds .exe on Windows; npm, pnpm and gem are .cmd shims found via PATHEXT.
|
// Command::new only finds .exe on Windows; npm, pnpm and gem are .cmd shims found via PATHEXT.
|
||||||
fn resolve(program: &str) -> std::io::Result<PathBuf> {
|
fn resolve(program: &str) -> std::io::Result<PathBuf> {
|
||||||
which::which(program).map_err(|e| Error::new(ErrorKind::NotFound, e))
|
which::which(program).map_err(|e| Error::new(ErrorKind::NotFound, e))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn warn_if_plaintext(lockfile: &str, dir: Option<&Path>) {
|
||||||
|
let path = dir.unwrap_or(Path::new(".")).join(lockfile);
|
||||||
|
let Ok(text) = std::fs::read_to_string(path) else { return };
|
||||||
|
if BUILT_IN.iter().any(|registry| text.contains(&format!("http://{}/", registry.host()))) {
|
||||||
|
warn!("{lockfile} has http:// registry URLs; change them to https:// before committing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_https(lockfile: &str, dir: Option<&Path>) {
|
||||||
|
let path = dir.unwrap_or(Path::new(".")).join(lockfile);
|
||||||
|
let Ok(text) = std::fs::read_to_string(&path) else { return };
|
||||||
|
let mut fixed = text.clone();
|
||||||
|
for registry in BUILT_IN {
|
||||||
|
let host = registry.host();
|
||||||
|
fixed = fixed.replace(&format!("http://{host}/"), &format!("https://{host}/"));
|
||||||
|
}
|
||||||
|
if fixed == text {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match std::fs::write(&path, fixed) {
|
||||||
|
Ok(()) => info!("changed {lockfile}'s registry URLs back to https://"),
|
||||||
|
Err(e) => warn!("could not change {lockfile}'s registry URLs back to https://: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,32 @@
|
|||||||
//! How the tray asks the service what it blocked, as the macOS app messages its extension.
|
//! How the tray asks the service what it blocked, and allows a new package once, as the macOS
|
||||||
|
//! app messages its extension.
|
||||||
|
|
||||||
use super::security_descriptor;
|
use super::{config_file, security_descriptor};
|
||||||
use crate::blocks;
|
use crate::{blocks, Firewall};
|
||||||
use rama::error::BoxError;
|
use rama::error::BoxError;
|
||||||
|
use std::os::windows::io::AsRawHandle;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
|
||||||
use tokio::net::windows::named_pipe::{NamedPipeServer, ServerOptions};
|
use tokio::net::windows::named_pipe::{NamedPipeServer, ServerOptions};
|
||||||
|
use windows::core::{Owned, PWSTR};
|
||||||
|
use windows::Win32::Foundation::HANDLE;
|
||||||
use windows::Win32::Security::SECURITY_ATTRIBUTES;
|
use windows::Win32::Security::SECURITY_ATTRIBUTES;
|
||||||
|
use windows::Win32::System::Pipes::GetNamedPipeClientProcessId;
|
||||||
|
use windows::Win32::System::Threading::*;
|
||||||
|
|
||||||
pub(super) const PIPE: &str = r"\\.\pipe\StrixFirewall";
|
pub(super) const PIPE: &str = r"\\.\pipe\StrixFirewall";
|
||||||
/// Signed-in users can connect, as the tray runs as them.
|
/// Signed-in users can connect, as the tray runs as them.
|
||||||
const PIPE_ACL: &str = "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;IU)";
|
const PIPE_ACL: &str = "D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;IU)";
|
||||||
|
|
||||||
pub(super) async fn serve() -> Result<(), BoxError> {
|
pub(super) async fn serve(firewall: Arc<Firewall>) -> Result<(), BoxError> {
|
||||||
let mut server = create(true)?;
|
let mut server = create(true)?;
|
||||||
loop {
|
loop {
|
||||||
server.connect().await?;
|
server.connect().await?;
|
||||||
let client = std::mem::replace(&mut server, create(false)?);
|
let client = std::mem::replace(&mut server, create(false)?);
|
||||||
|
let firewall = Arc::clone(&firewall);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(error) = answer(client).await {
|
if let Err(error) = answer(client, &firewall).await {
|
||||||
tracing::debug!(%error, "could not answer the tray");
|
tracing::debug!(%error, "could not answer the tray");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -40,19 +48,39 @@ fn create(first: bool) -> Result<NamedPipeServer, BoxError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A line each way.
|
/// A line each way.
|
||||||
async fn answer(client: NamedPipeServer) -> Result<(), BoxError> {
|
async fn answer(client: NamedPipeServer, firewall: &Firewall) -> Result<(), BoxError> {
|
||||||
let mut client = BufReader::new(client);
|
let mut client = BufReader::new(client);
|
||||||
let mut request = String::new();
|
let mut request = String::new();
|
||||||
(&mut client)
|
(&mut client)
|
||||||
.take(64 * 1024)
|
.take(64 * 1024)
|
||||||
.read_line(&mut request)
|
.read_line(&mut request)
|
||||||
.await?;
|
.await?;
|
||||||
let reply = match request.trim_end() {
|
let request = request.trim_end();
|
||||||
"blocks" => serde_json::to_string(&blocks::take())?,
|
// Only the app can message the macOS extension; here any program could connect.
|
||||||
other => format!("error: unknown request {other}"),
|
let reply = if !client_is_tray(client.get_ref())? {
|
||||||
|
"error: only the tray can ask".to_owned()
|
||||||
|
} else if request.starts_with("allow ") {
|
||||||
|
// Trusting writes the config for every user, so it is left to an administrator.
|
||||||
|
"error: trusting a package takes an administrator".to_owned()
|
||||||
|
} else {
|
||||||
|
blocks::answer(firewall, &config_file(), request)
|
||||||
};
|
};
|
||||||
client.write_all(format!("{reply}\n").as_bytes()).await?;
|
client.write_all(format!("{reply}\n").as_bytes()).await?;
|
||||||
// Closing before the tray hangs up could discard the reply.
|
// Closing before the tray hangs up could discard the reply.
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(5), client.read(&mut [0])).await;
|
let _ = tokio::time::timeout(Duration::from_secs(5), client.read(&mut [0])).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn client_is_tray(client: &NamedPipeServer) -> Result<bool, BoxError> {
|
||||||
|
let mut id = 0;
|
||||||
|
unsafe { GetNamedPipeClientProcessId(HANDLE(client.as_raw_handle()), &mut id)? };
|
||||||
|
let process = unsafe { Owned::new(OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, id)?) };
|
||||||
|
let mut path = [0u16; 1024];
|
||||||
|
let mut len = path.len() as u32;
|
||||||
|
let flags = PROCESS_NAME_WIN32;
|
||||||
|
unsafe { QueryFullProcessImageNameW(*process, flags, PWSTR(path.as_mut_ptr()), &mut len)? };
|
||||||
|
let path = String::from_utf16(&path[..len as usize])?;
|
||||||
|
let tray = std::env::current_exe()?.with_file_name("firewall-tray.exe");
|
||||||
|
// Windows paths ignore case.
|
||||||
|
Ok(path.to_lowercase() == tray.to_string_lossy().to_lowercase())
|
||||||
|
}
|
||||||
@@ -155,8 +155,9 @@ async fn start_screening(status: &ServiceStatusHandle) -> Result<Vec<&'static st
|
|||||||
let crl = HttpServer::auto(exec.clone()).service(crl);
|
let crl = HttpServer::auto(exec.clone()).service(crl);
|
||||||
tokio::spawn(listen_on(CRL_ADDRESS, 80, &exec)?.serve(crl));
|
tokio::spawn(listen_on(CRL_ADDRESS, 80, &exec)?.serve(crl));
|
||||||
|
|
||||||
tokio::spawn(async {
|
let tray_firewall = Arc::clone(&firewall);
|
||||||
if let Err(error) = pipe::serve().await {
|
tokio::spawn(async move {
|
||||||
|
if let Err(error) = pipe::serve(tray_firewall).await {
|
||||||
tracing::error!(%error, "the tray cannot reach the service");
|
tracing::error!(%error, "the tray cannot reach the service");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
use super::pipe::PIPE;
|
use super::pipe::PIPE;
|
||||||
use super::*;
|
use super::*;
|
||||||
use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
|
use muda::{IsMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::io::{BufRead, BufReader, Cursor, Write};
|
use std::io::{BufRead, BufReader, Cursor, Write};
|
||||||
use std::os::windows::io::AsRawHandle;
|
use std::os::windows::io::AsRawHandle;
|
||||||
|
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tray_icon::{Icon, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
use tray_icon::{Icon, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
|
||||||
use windows::core::{w, PCWSTR};
|
use windows::core::{w, PCWSTR};
|
||||||
@@ -22,6 +23,8 @@ const MENU_PAUSE: &str = "pause";
|
|||||||
const MENU_RESTART: &str = "restart";
|
const MENU_RESTART: &str = "restart";
|
||||||
const MENU_LOG: &str = "log";
|
const MENU_LOG: &str = "log";
|
||||||
const MENU_QUIT: &str = "quit";
|
const MENU_QUIT: &str = "quit";
|
||||||
|
/// Also the service's request.
|
||||||
|
const ALLOW_ONCE: &str = "allow-once";
|
||||||
const LOGO: &[u8] = include_bytes!("tray.png");
|
const LOGO: &[u8] = include_bytes!("tray.png");
|
||||||
/// Where blocked packages are listed, below Open logs.
|
/// Where blocked packages are listed, below Open logs.
|
||||||
const BLOCKED_AT: usize = 5;
|
const BLOCKED_AT: usize = 5;
|
||||||
@@ -62,9 +65,11 @@ pub fn run() -> Result<(), BoxError> {
|
|||||||
menu,
|
menu,
|
||||||
blocked_separator,
|
blocked_separator,
|
||||||
blocked: Vec::new(),
|
blocked: Vec::new(),
|
||||||
|
allowed: channel(),
|
||||||
};
|
};
|
||||||
tray.update();
|
tray.update();
|
||||||
unsafe { SetTimer(None, 0, 2000, None) };
|
// The service holds a new package's download only while it is asked this often.
|
||||||
|
unsafe { SetTimer(None, 0, 500, None) };
|
||||||
let mut message = MSG::default();
|
let mut message = MSG::default();
|
||||||
while unsafe { GetMessageW(&mut message, None, 0, 0) }.0 > 0 {
|
while unsafe { GetMessageW(&mut message, None, 0, 0) }.0 > 0 {
|
||||||
unsafe { DispatchMessageW(&message) };
|
unsafe { DispatchMessageW(&message) };
|
||||||
@@ -112,10 +117,13 @@ struct Tray {
|
|||||||
blocked_separator: PredefinedMenuItem,
|
blocked_separator: PredefinedMenuItem,
|
||||||
/// The most recent, as the menu lists them.
|
/// The most recent, as the menu lists them.
|
||||||
blocked: Vec<BlockedPackage>,
|
blocked: Vec<BlockedPackage>,
|
||||||
|
/// Allowed from another thread, so no longer listed.
|
||||||
|
allowed: (Sender<BlockedPackage>, Receiver<BlockedPackage>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Deserialize)]
|
#[derive(Clone, PartialEq, Deserialize)]
|
||||||
struct BlockedPackage {
|
struct BlockedPackage {
|
||||||
|
id: u64,
|
||||||
eco: String,
|
eco: String,
|
||||||
name: String,
|
name: String,
|
||||||
version: Option<String>,
|
version: Option<String>,
|
||||||
@@ -136,6 +144,11 @@ impl BlockedPackage {
|
|||||||
fn malicious(&self) -> bool {
|
fn malicious(&self) -> bool {
|
||||||
self.filter != "age"
|
self.filter != "age"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Malware is never allowed, and a new package only by version.
|
||||||
|
fn trustable(&self) -> bool {
|
||||||
|
!self.malicious() && self.version.is_some()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tray {
|
impl Tray {
|
||||||
@@ -161,6 +174,11 @@ impl Tray {
|
|||||||
|
|
||||||
/// The service hands each block over once, so whichever tray asks first shows it.
|
/// The service hands each block over once, so whichever tray asks first shows it.
|
||||||
fn collect_blocks(&mut self) {
|
fn collect_blocks(&mut self) {
|
||||||
|
let allowed: Vec<_> = self.allowed.1.try_iter().collect();
|
||||||
|
if !allowed.is_empty() {
|
||||||
|
self.blocked.retain(|blocked| !allowed.contains(blocked));
|
||||||
|
self.list_blocked();
|
||||||
|
}
|
||||||
if self.state != Some(ServiceState::Running) {
|
if self.state != Some(ServiceState::Running) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -184,7 +202,14 @@ impl Tray {
|
|||||||
self.blocked.drain(..excess);
|
self.blocked.drain(..excess);
|
||||||
self.list_blocked();
|
self.list_blocked();
|
||||||
// Off the UI thread, so the menu keeps working while they are open.
|
// Off the UI thread, so the menu keeps working while they are open.
|
||||||
std::thread::spawn(move || new.iter().for_each(alert));
|
let allowed = self.allowed.0.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
for package in new {
|
||||||
|
if alert(&package) {
|
||||||
|
allow_once(package, &allowed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_blocked(&self) {
|
fn list_blocked(&self) {
|
||||||
@@ -204,14 +229,28 @@ impl Tray {
|
|||||||
package.reason.clone()
|
package.reason.clone()
|
||||||
};
|
};
|
||||||
let reason = MenuItem::new(reason, false, None);
|
let reason = MenuItem::new(reason, false, None);
|
||||||
|
let once = MenuItem::with_id(format!("{ALLOW_ONCE}:{i}"), "Allow Once", true, None);
|
||||||
|
let items: &[&dyn IsMenuItem] = if package.trustable() {
|
||||||
|
&[&reason, &once]
|
||||||
|
} else {
|
||||||
|
&[&reason]
|
||||||
|
};
|
||||||
let label = format!("{verb} {}", package.label());
|
let label = format!("{verb} {}", package.label());
|
||||||
if let Ok(item) = Submenu::with_items(label, true, &[&reason]) {
|
if let Ok(item) = Submenu::with_items(label, true, items) {
|
||||||
let _ = self.menu.insert(&item, BLOCKED_AT + 1 + i);
|
let _ = self.menu.insert(&item, BLOCKED_AT + 1 + i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_menu(&self, id: &str) {
|
fn on_menu(&self, id: &str) {
|
||||||
|
if let Some(i) = id.strip_prefix(ALLOW_ONCE).and_then(|i| i.strip_prefix(':')) {
|
||||||
|
let package = i.parse().ok().and_then(|i: usize| self.blocked.get(i));
|
||||||
|
if let Some(package) = package.cloned() {
|
||||||
|
let allowed = self.allowed.0.clone();
|
||||||
|
std::thread::spawn(move || allow_once(package, &allowed));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
let state = self.state;
|
let state = self.state;
|
||||||
let screening = state == Some(ServiceState::Running);
|
let screening = state == Some(ServiceState::Running);
|
||||||
let action: fn() -> Result<(), BoxError> = match id {
|
let action: fn() -> Result<(), BoxError> = match id {
|
||||||
@@ -237,7 +276,8 @@ impl Tray {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn alert(package: &BlockedPackage) {
|
/// Whether the user chose to allow it once.
|
||||||
|
fn alert(package: &BlockedPackage) -> bool {
|
||||||
let text = format!("{} {}.", package.label(), package.reason);
|
let text = format!("{} {}.", package.label(), package.reason);
|
||||||
let (title, text, style) = if package.malicious() {
|
let (title, text, style) = if package.malicious() {
|
||||||
let title = format!("Blocked malware: {}", package.label());
|
let title = format!("Blocked malware: {}", package.label());
|
||||||
@@ -245,11 +285,33 @@ fn alert(package: &BlockedPackage) {
|
|||||||
+ "\n\nIt was not installed. Don't install it from another source. If you didn't expect this "
|
+ "\n\nIt was not installed. Don't install it from another source. If you didn't expect this "
|
||||||
+ "package, a dependency may be compromised; tell your security team.";
|
+ "package, a dependency may be compromised; tell your security team.";
|
||||||
(title, text, MB_ICONERROR)
|
(title, text, MB_ICONERROR)
|
||||||
} else {
|
} else if !package.trustable() {
|
||||||
("Held back a new package".to_owned(), text, MB_ICONINFORMATION)
|
("Held back a new package".to_owned(), text, MB_ICONINFORMATION)
|
||||||
|
} else {
|
||||||
|
let text = text + "\n\nAllow it once?";
|
||||||
|
("Held back a new package".to_owned(), text, MB_YESNO | MB_ICONINFORMATION)
|
||||||
};
|
};
|
||||||
let (title, text) = (HSTRING::from(title), HSTRING::from(text));
|
let (title, text) = (HSTRING::from(title), HSTRING::from(text));
|
||||||
unsafe { MessageBoxW(None, &text, &title, style | MB_SETFOREGROUND) };
|
unsafe { MessageBoxW(None, &text, &title, style | MB_SETFOREGROUND) == IDYES }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tells the user when the install had already stopped waiting.
|
||||||
|
fn allow_once(package: BlockedPackage, allowed: &Sender<BlockedPackage>) {
|
||||||
|
let reply = match ask_service(&format!("{ALLOW_ONCE} {}", package.id)) {
|
||||||
|
Ok(reply) => reply,
|
||||||
|
Err(error) => return show_error(error),
|
||||||
|
};
|
||||||
|
match reply.as_str() {
|
||||||
|
"ok" => {}
|
||||||
|
"late" => {
|
||||||
|
let title = format!("Allowed {} for the next minute", package.label());
|
||||||
|
let text = "The install had already stopped waiting for it. Run it again to finish.";
|
||||||
|
let (title, text) = (HSTRING::from(title), HSTRING::from(text));
|
||||||
|
unsafe { MessageBoxW(None, &text, &title, MB_ICONINFORMATION | MB_SETFOREGROUND) };
|
||||||
|
}
|
||||||
|
other => return show_error(other.into()),
|
||||||
|
}
|
||||||
|
let _ = allowed.send(package);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One request per connection, as the service answers.
|
/// One request per connection, as the service answers.
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ impl Filter for Osv {
|
|||||||
if hits.is_empty() {
|
if hits.is_empty() {
|
||||||
Verdict::Allow
|
Verdict::Allow
|
||||||
} else {
|
} else {
|
||||||
Verdict::Deny(format!("is flagged malicious by OSV ({})", hits.join(", ")))
|
let links: Vec<String> =
|
||||||
|
hits.iter().map(|id| format!("{id} https://osv.dev/vulnerability/{id}")).collect();
|
||||||
|
Verdict::Deny(format!("is flagged malicious by OSV: {}", links.join(", ")))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ enum BlockAlert {
|
|||||||
if package.malicious {
|
if package.malicious {
|
||||||
alert.alertStyle = .critical
|
alert.alertStyle = .critical
|
||||||
alert.messageText = "Blocked malware: \(package.label) (\(package.eco))"
|
alert.messageText = "Blocked malware: \(package.label) (\(package.eco))"
|
||||||
alert.informativeText = "\(package.label) \(package.reason).\n\n"
|
setText(alert, "\(package.label) \(package.reason).\n\n"
|
||||||
+ "It was not installed. Don't install it from another source. "
|
+ "It was not installed. Don't install it from another source. "
|
||||||
+ "If you didn't expect this package, a dependency may be compromised; tell your security team."
|
+ "If you didn't expect this package, a dependency may be compromised; tell your security team.")
|
||||||
} else {
|
} else {
|
||||||
alert.messageText = "Held back a new package"
|
alert.messageText = "Held back a new package"
|
||||||
alert.informativeText = "\(package.label) (\(package.eco)) \(package.reason)."
|
setText(alert, "\(package.label) (\(package.eco)) \(package.reason).")
|
||||||
if package.trustable {
|
if package.trustable {
|
||||||
alert.addButton(withTitle: "Allow Once")
|
alert.addButton(withTitle: "Allow Once")
|
||||||
alert.addButton(withTitle: "Trust this version")
|
alert.addButton(withTitle: "Trust this version")
|
||||||
@@ -56,6 +56,22 @@ enum BlockAlert {
|
|||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
alert.runModal()
|
alert.runModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An alert's own text is plain, so this text goes in a selectable field, where its links open on click.
|
||||||
|
private static func setText(_ alert: NSAlert, _ text: String) {
|
||||||
|
let linked = NSMutableAttributedString(string: text, attributes: [.font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)])
|
||||||
|
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
||||||
|
for match in detector.matches(in: text, range: NSRange(text.startIndex..., in: text)) {
|
||||||
|
if let url = match.url { linked.addAttribute(.link, value: url, range: match.range) }
|
||||||
|
}
|
||||||
|
let field = NSTextField(wrappingLabelWithString: "")
|
||||||
|
field.allowsEditingTextAttributes = true
|
||||||
|
field.isSelectable = true
|
||||||
|
field.attributedStringValue = linked
|
||||||
|
field.preferredMaxLayoutWidth = 300
|
||||||
|
field.frame.size = field.fittingSize
|
||||||
|
alert.accessoryView = field
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AdminAuthorization {
|
enum AdminAuthorization {
|
||||||
|
|||||||
Reference in new issue
Block a user