Fix 29 audit findings across all severity tiers
Critical: fix unsquashfs arg order, quote Exec paths with spaces, fix compare_versions antisymmetry, chunk-based signature detection, bounded ELF header reads. High: handle NULL CVE severity, prevent pipe deadlock in inspector, fix glob_match edge case, fix backup archive path collisions, async crash detection with stderr capture. Medium: gate scan on auto-scan setting, fix window size persistence, fix announce() for Stack containers, claim lightbox gesture, use serde_json for CLI output, remove dead CSS @media blocks, add detail-tab persistence, remove invalid metainfo categories, byte-level fuse signature search. Low: tighten Wayland env var detection, ELF magic validation, timeout for update info extraction, quoted arg parsing, stop watcher timer on window destroy, GSettings choices/range constraints, remove unused CSS classes, define status-ok/status-attention CSS.
This commit is contained in:
@@ -15,7 +15,6 @@ const MAX_CONCURRENT_ANALYSES: usize = 2;
|
||||
static RUNNING_ANALYSES: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Returns the number of currently running background analyses.
|
||||
#[allow(dead_code)]
|
||||
pub fn running_count() -> usize {
|
||||
RUNNING_ANALYSES.load(Ordering::Relaxed)
|
||||
}
|
||||
@@ -64,6 +63,10 @@ pub fn run_background_analysis(id: i64, path: PathBuf, appimage_type: AppImageTy
|
||||
|
||||
// Inspect metadata (app name, version, icon, desktop entry, AppStream, etc.)
|
||||
if let Ok(meta) = inspector::inspect_appimage(&path, &appimage_type) {
|
||||
log::debug!(
|
||||
"Metadata for id={}: name={:?}, icon_name={:?}",
|
||||
id, meta.app_name.as_deref(), meta.icon_name.as_deref(),
|
||||
);
|
||||
let categories = if meta.categories.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -405,7 +405,6 @@ fn summarize_content_rating(attrs: &[(String, String)]) -> String {
|
||||
// AppStream catalog generation - writes catalog XML for GNOME Software/Discover
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Generate an AppStream catalog XML from the Driftwood database.
|
||||
/// This allows GNOME Software / KDE Discover to see locally managed AppImages.
|
||||
pub fn generate_catalog(db: &Database) -> Result<String, AppStreamError> {
|
||||
@@ -463,7 +462,6 @@ pub fn generate_catalog(db: &Database) -> Result<String, AppStreamError> {
|
||||
Ok(xml)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Install the AppStream catalog to the local swcatalog directory.
|
||||
/// GNOME Software reads from `~/.local/share/swcatalog/xml/`.
|
||||
pub fn install_catalog(db: &Database) -> Result<PathBuf, AppStreamError> {
|
||||
@@ -484,7 +482,6 @@ pub fn install_catalog(db: &Database) -> Result<PathBuf, AppStreamError> {
|
||||
Ok(catalog_path)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Remove the AppStream catalog from the local swcatalog directory.
|
||||
pub fn uninstall_catalog() -> Result<(), AppStreamError> {
|
||||
let catalog_path = dirs::data_dir()
|
||||
@@ -501,7 +498,6 @@ pub fn uninstall_catalog() -> Result<(), AppStreamError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Check if the AppStream catalog is currently installed.
|
||||
pub fn is_catalog_installed() -> bool {
|
||||
let catalog_path = dirs::data_dir()
|
||||
@@ -515,7 +511,6 @@ pub fn is_catalog_installed() -> bool {
|
||||
|
||||
// --- Utility functions ---
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn make_component_id(name: &str) -> String {
|
||||
name.chars()
|
||||
.map(|c| if c.is_alphanumeric() || c == '-' || c == '.' { c.to_ascii_lowercase() } else { '_' })
|
||||
@@ -524,7 +519,6 @@ fn make_component_id(name: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn xml_escape(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
@@ -536,7 +530,6 @@ fn xml_escape(s: &str) -> String {
|
||||
// --- Error types ---
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum AppStreamError {
|
||||
Database(String),
|
||||
Io(String),
|
||||
|
||||
@@ -119,16 +119,23 @@ pub fn create_backup(db: &Database, appimage_id: i64) -> Result<PathBuf, BackupE
|
||||
"manifest.json".to_string(),
|
||||
];
|
||||
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
|
||||
for entry in &entries {
|
||||
let source = Path::new(&entry.original_path);
|
||||
if source.exists() {
|
||||
tar_args.push("-C".to_string());
|
||||
tar_args.push(
|
||||
source.parent().unwrap_or(Path::new("/")).to_string_lossy().to_string(),
|
||||
);
|
||||
tar_args.push(
|
||||
source.file_name().unwrap_or_default().to_string_lossy().to_string(),
|
||||
);
|
||||
if let Ok(rel) = source.strip_prefix(&home_dir) {
|
||||
tar_args.push("-C".to_string());
|
||||
tar_args.push(home_dir.to_string_lossy().to_string());
|
||||
tar_args.push(rel.to_string_lossy().to_string());
|
||||
} else {
|
||||
tar_args.push("-C".to_string());
|
||||
tar_args.push(
|
||||
source.parent().unwrap_or(Path::new("/")).to_string_lossy().to_string(),
|
||||
);
|
||||
tar_args.push(
|
||||
source.file_name().unwrap_or_default().to_string_lossy().to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,12 +197,16 @@ pub fn restore_backup(archive_path: &Path) -> Result<RestoreResult, BackupError>
|
||||
// Restore each path
|
||||
let mut restored = 0u32;
|
||||
let mut skipped = 0u32;
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
|
||||
|
||||
for entry in &manifest.paths {
|
||||
let source_name = Path::new(&entry.original_path)
|
||||
.file_name()
|
||||
.unwrap_or_default();
|
||||
let extracted = temp_dir.path().join(source_name);
|
||||
let source = Path::new(&entry.original_path);
|
||||
let extracted = if let Ok(rel) = source.strip_prefix(&home_dir) {
|
||||
temp_dir.path().join(rel)
|
||||
} else {
|
||||
let source_name = source.file_name().unwrap_or_default();
|
||||
temp_dir.path().join(source_name)
|
||||
};
|
||||
let target = Path::new(&entry.original_path);
|
||||
|
||||
if !extracted.exists() {
|
||||
@@ -269,7 +280,6 @@ pub fn delete_backup(db: &Database, backup_id: i64) -> Result<(), BackupError> {
|
||||
}
|
||||
|
||||
/// Remove backups older than the specified number of days.
|
||||
#[allow(dead_code)]
|
||||
pub fn auto_cleanup_old_backups(db: &Database, retention_days: u32) -> Result<u32, BackupError> {
|
||||
let backups = db.get_all_config_backups().unwrap_or_default();
|
||||
let cutoff = chrono::Utc::now() - chrono::Duration::days(retention_days as i64);
|
||||
@@ -292,7 +302,6 @@ pub fn auto_cleanup_old_backups(db: &Database, retention_days: u32) -> Result<u3
|
||||
#[derive(Debug)]
|
||||
pub struct BackupInfo {
|
||||
pub id: i64,
|
||||
#[allow(dead_code)]
|
||||
pub appimage_id: i64,
|
||||
pub app_version: Option<String>,
|
||||
pub archive_path: String,
|
||||
@@ -304,10 +313,8 @@ pub struct BackupInfo {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RestoreResult {
|
||||
#[allow(dead_code)]
|
||||
pub manifest: BackupManifest,
|
||||
pub paths_restored: u32,
|
||||
#[allow(dead_code)]
|
||||
pub paths_skipped: u32,
|
||||
}
|
||||
|
||||
|
||||
@@ -184,34 +184,6 @@ pub struct ConfigBackupRecord {
|
||||
pub last_restored_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct CatalogSourceRecord {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub source_type: String,
|
||||
pub enabled: bool,
|
||||
pub last_synced: Option<String>,
|
||||
pub app_count: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct CatalogAppRecord {
|
||||
pub id: i64,
|
||||
pub source_id: i64,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub categories: Option<String>,
|
||||
pub latest_version: Option<String>,
|
||||
pub download_url: String,
|
||||
pub icon_url: Option<String>,
|
||||
pub homepage: Option<String>,
|
||||
pub file_size: Option<i64>,
|
||||
pub architecture: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SandboxProfileRecord {
|
||||
pub id: i64,
|
||||
@@ -1374,7 +1346,9 @@ impl Database {
|
||||
WHERE appimage_id = ?1 GROUP BY severity"
|
||||
)?;
|
||||
let rows = stmt.query_map(params![appimage_id], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
|
||||
let severity: String = row.get::<_, Option<String>>(0)?
|
||||
.unwrap_or_else(|| "MEDIUM".to_string());
|
||||
Ok((severity, row.get::<_, i64>(1)?))
|
||||
})?;
|
||||
for row in rows {
|
||||
let (severity, count) = row?;
|
||||
@@ -1395,7 +1369,9 @@ impl Database {
|
||||
"SELECT severity, COUNT(*) FROM cve_matches GROUP BY severity"
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
|
||||
let severity: String = row.get::<_, Option<String>>(0)?
|
||||
.unwrap_or_else(|| "MEDIUM".to_string());
|
||||
Ok((severity, row.get::<_, i64>(1)?))
|
||||
})?;
|
||||
for row in rows {
|
||||
let (severity, count) = row?;
|
||||
|
||||
@@ -330,9 +330,12 @@ fn build_name_group(name: &str, records: &[&AppImageRecord]) -> DuplicateGroup {
|
||||
|
||||
/// Compare two version strings for ordering.
|
||||
fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering {
|
||||
use super::updater::version_is_newer;
|
||||
use super::updater::{clean_version, version_is_newer};
|
||||
|
||||
if a == b {
|
||||
let ca = clean_version(a);
|
||||
let cb = clean_version(b);
|
||||
|
||||
if ca == cb {
|
||||
std::cmp::Ordering::Equal
|
||||
} else if version_is_newer(a, b) {
|
||||
std::cmp::Ordering::Greater
|
||||
|
||||
@@ -186,9 +186,20 @@ fn has_static_runtime(appimage_path: &Path) -> bool {
|
||||
Err(_) => return false,
|
||||
};
|
||||
let data = &buf[..n];
|
||||
let haystack = String::from_utf8_lossy(data).to_lowercase();
|
||||
haystack.contains("type2-runtime")
|
||||
|| haystack.contains("libfuse3")
|
||||
// Search raw bytes directly - avoids allocating a UTF-8 string from binary data.
|
||||
// Case-insensitive matching for the two known signatures.
|
||||
bytes_contains_ci(data, b"type2-runtime")
|
||||
|| bytes_contains_ci(data, b"libfuse3")
|
||||
}
|
||||
|
||||
/// Case-insensitive byte-level substring search (ASCII only).
|
||||
fn bytes_contains_ci(haystack: &[u8], needle: &[u8]) -> bool {
|
||||
if needle.is_empty() || haystack.len() < needle.len() {
|
||||
return false;
|
||||
}
|
||||
haystack.windows(needle.len()).any(|window| {
|
||||
window.iter().zip(needle).all(|(h, n)| h.to_ascii_lowercase() == n.to_ascii_lowercase())
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if --appimage-extract-and-run is supported.
|
||||
|
||||
@@ -38,7 +38,6 @@ pub struct AppImageMetadata {
|
||||
pub app_version: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub developer: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
pub icon_name: Option<String>,
|
||||
pub categories: Vec<String>,
|
||||
pub desktop_entry_content: String,
|
||||
@@ -246,7 +245,7 @@ fn extract_metadata_files(
|
||||
.arg("usr/share/metainfo/*.xml")
|
||||
.arg("usr/share/appdata/*.xml")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status();
|
||||
|
||||
match status {
|
||||
@@ -430,8 +429,20 @@ fn detect_architecture(path: &Path) -> Option<String> {
|
||||
let mut header = [0u8; 20];
|
||||
file.read_exact(&mut header).ok()?;
|
||||
|
||||
// ELF e_machine at offset 18 (little-endian)
|
||||
let machine = u16::from_le_bytes([header[18], header[19]]);
|
||||
// Validate ELF magic
|
||||
if &header[0..4] != b"\x7FELF" {
|
||||
return None;
|
||||
}
|
||||
|
||||
// ELF e_machine at offset 18, endianness from byte 5
|
||||
let machine = if header[5] == 2 {
|
||||
// Big-endian
|
||||
u16::from_be_bytes([header[18], header[19]])
|
||||
} else {
|
||||
// Little-endian (default)
|
||||
u16::from_le_bytes([header[18], header[19]])
|
||||
};
|
||||
|
||||
match machine {
|
||||
0x03 => Some("i386".to_string()),
|
||||
0x3E => Some("x86_64".to_string()),
|
||||
@@ -529,12 +540,42 @@ fn find_appstream_file(extract_dir: &Path) -> Option<PathBuf> {
|
||||
|
||||
/// Check if an AppImage has a GPG signature by looking for the .sha256_sig section name.
|
||||
fn detect_signature(path: &Path) -> bool {
|
||||
let data = match fs::read(path) {
|
||||
Ok(d) => d,
|
||||
use std::io::{BufReader, Read};
|
||||
let file = match fs::File::open(path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let needle = b".sha256_sig";
|
||||
data.windows(needle.len()).any(|w| w == needle)
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut buf = vec![0u8; 64 * 1024];
|
||||
let mut carry = Vec::new();
|
||||
|
||||
loop {
|
||||
let n = match reader.read(&mut buf) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => n,
|
||||
Err(_) => break,
|
||||
};
|
||||
// Prepend carry bytes from previous chunk to handle needle spanning chunks
|
||||
let search_buf = if carry.is_empty() {
|
||||
&buf[..n]
|
||||
} else {
|
||||
carry.extend_from_slice(&buf[..n]);
|
||||
carry.as_slice()
|
||||
};
|
||||
if search_buf.windows(needle.len()).any(|w| w == needle) {
|
||||
return true;
|
||||
}
|
||||
// Keep the last (needle.len - 1) bytes as carry for the next iteration
|
||||
let keep = needle.len() - 1;
|
||||
carry.clear();
|
||||
if n >= keep {
|
||||
carry.extend_from_slice(&buf[n - keep..n]);
|
||||
} else {
|
||||
carry.extend_from_slice(&buf[..n]);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Cache an icon file to the driftwood icons directory.
|
||||
|
||||
@@ -94,7 +94,7 @@ pub fn integrate(record: &AppImageRecord) -> Result<IntegrationResult, Integrati
|
||||
"[Desktop Entry]\n\
|
||||
Type=Application\n\
|
||||
Name={name}\n\
|
||||
Exec={exec} %U\n\
|
||||
Exec=\"{exec}\" %U\n\
|
||||
Icon={icon}\n\
|
||||
Categories={categories}\n\
|
||||
Comment={comment}\n\
|
||||
@@ -228,7 +228,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_integrate_creates_desktop_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let _dir = tempfile::tempdir().unwrap();
|
||||
// Override the applications dir for testing by creating the record
|
||||
// with a specific path and testing the desktop content generation
|
||||
let record = AppImageRecord {
|
||||
|
||||
@@ -42,7 +42,6 @@ pub enum LaunchMethod {
|
||||
/// Extract-and-run fallback (APPIMAGE_EXTRACT_AND_RUN=1)
|
||||
ExtractAndRun,
|
||||
/// Via firejail sandbox
|
||||
#[allow(dead_code)]
|
||||
Sandboxed,
|
||||
}
|
||||
|
||||
@@ -68,7 +67,6 @@ pub enum LaunchResult {
|
||||
Crashed {
|
||||
exit_code: Option<i32>,
|
||||
stderr: String,
|
||||
#[allow(dead_code)]
|
||||
method: LaunchMethod,
|
||||
},
|
||||
/// Failed to launch.
|
||||
@@ -99,6 +97,22 @@ pub fn launch_appimage(
|
||||
}
|
||||
};
|
||||
|
||||
// Override with sandboxed launch if the user enabled firejail for this app
|
||||
let method = if has_firejail() {
|
||||
let sandbox = db
|
||||
.get_appimage_by_id(record_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|r| r.sandbox_mode);
|
||||
if sandbox.as_deref() == Some("firejail") {
|
||||
LaunchMethod::Sandboxed
|
||||
} else {
|
||||
method
|
||||
}
|
||||
} else {
|
||||
method
|
||||
};
|
||||
|
||||
let result = execute_appimage(appimage_path, &method, extra_args, extra_env);
|
||||
|
||||
// Record the launch event regardless of success
|
||||
@@ -163,42 +177,38 @@ fn execute_appimage(
|
||||
cmd.env(key, value);
|
||||
}
|
||||
|
||||
// Capture stderr to detect crash messages, stdin detached
|
||||
// Detach stdin, pipe stderr so we can capture crash messages
|
||||
cmd.stdin(Stdio::null());
|
||||
cmd.stderr(Stdio::piped());
|
||||
|
||||
match cmd.spawn() {
|
||||
Ok(mut child) => {
|
||||
// Brief wait to detect immediate crashes (e.g. missing Qt plugins)
|
||||
std::thread::sleep(std::time::Duration::from_millis(1500));
|
||||
// Give the process a brief moment to fail on immediate errors
|
||||
// (missing libs, exec format errors, Qt plugin failures, etc.)
|
||||
std::thread::sleep(std::time::Duration::from_millis(150));
|
||||
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
// Process already exited - it crashed
|
||||
let stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.and_then(|mut err| {
|
||||
let mut buf = String::new();
|
||||
use std::io::Read;
|
||||
err.read_to_string(&mut buf).ok()?;
|
||||
Some(buf)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// Already exited - immediate crash. Read stderr for details.
|
||||
let stderr_text = child.stderr.take().map(|mut pipe| {
|
||||
let mut buf = String::new();
|
||||
use std::io::Read;
|
||||
// Read with a size cap to avoid huge allocations
|
||||
let mut limited = (&mut pipe).take(64 * 1024);
|
||||
let _ = limited.read_to_string(&mut buf);
|
||||
buf
|
||||
}).unwrap_or_default();
|
||||
|
||||
LaunchResult::Crashed {
|
||||
exit_code: status.code(),
|
||||
stderr,
|
||||
stderr: stderr_text,
|
||||
method: method.clone(),
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// Still running - success
|
||||
LaunchResult::Started {
|
||||
child,
|
||||
method: method.clone(),
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Can't check status, assume it's running
|
||||
_ => {
|
||||
// Still running after 150ms - drop the stderr pipe so the
|
||||
// child process won't block if it fills the pipe buffer.
|
||||
drop(child.stderr.take());
|
||||
LaunchResult::Started {
|
||||
child,
|
||||
method: method.clone(),
|
||||
@@ -211,11 +221,38 @@ fn execute_appimage(
|
||||
}
|
||||
|
||||
/// Parse a launch_args string from the database into a Vec of individual arguments.
|
||||
/// Splits on whitespace; returns an empty Vec if the input is None or empty.
|
||||
#[allow(dead_code)]
|
||||
/// Parse launch arguments with basic quote support.
|
||||
/// Splits on whitespace, respecting double-quoted strings.
|
||||
/// Returns an empty Vec if the input is None or empty.
|
||||
pub fn parse_launch_args(args: Option<&str>) -> Vec<String> {
|
||||
args.map(|s| s.split_whitespace().map(String::from).collect())
|
||||
.unwrap_or_default()
|
||||
let Some(s) = args else {
|
||||
return Vec::new();
|
||||
};
|
||||
let s = s.trim();
|
||||
if s.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
let mut current = String::new();
|
||||
let mut in_quotes = false;
|
||||
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'"' => in_quotes = !in_quotes,
|
||||
' ' | '\t' if !in_quotes => {
|
||||
if !current.is_empty() {
|
||||
result.push(std::mem::take(&mut current));
|
||||
}
|
||||
}
|
||||
_ => current.push(c),
|
||||
}
|
||||
}
|
||||
if !current.is_empty() {
|
||||
result.push(current);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Check if firejail is available for sandboxed launches.
|
||||
|
||||
@@ -5,7 +5,6 @@ use super::security;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CveNotification {
|
||||
pub app_name: String,
|
||||
#[allow(dead_code)]
|
||||
pub appimage_id: i64,
|
||||
pub severity: String,
|
||||
pub cve_count: usize,
|
||||
@@ -138,7 +137,6 @@ fn send_desktop_notification(notif: &CveNotification) -> Result<(), Notification
|
||||
|
||||
/// Run a security scan and send notifications for any new findings.
|
||||
/// This is the CLI entry point for `driftwood security --notify`.
|
||||
#[allow(dead_code)]
|
||||
pub fn scan_and_notify(db: &Database, threshold: &str) -> Vec<CveNotification> {
|
||||
// First run a batch scan to get fresh data
|
||||
let _results = security::batch_scan(db);
|
||||
|
||||
@@ -10,7 +10,6 @@ pub enum ReportFormat {
|
||||
}
|
||||
|
||||
impl ReportFormat {
|
||||
#[allow(dead_code)]
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"json" => Some(Self::Json),
|
||||
@@ -20,7 +19,6 @@ impl ReportFormat {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn extension(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Json => "json",
|
||||
|
||||
@@ -28,7 +28,6 @@ pub struct CveMatch {
|
||||
/// Result of a security scan for a single AppImage.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SecurityScanResult {
|
||||
#[allow(dead_code)]
|
||||
pub appimage_id: i64,
|
||||
pub libraries: Vec<BundledLibrary>,
|
||||
pub cve_matches: Vec<(BundledLibrary, Vec<CveMatch>)>,
|
||||
@@ -254,10 +253,9 @@ pub fn detect_version_from_binary(
|
||||
let extract_output = Command::new("unsquashfs")
|
||||
.args(["-o", &offset, "-f", "-d"])
|
||||
.arg(temp_dir.path())
|
||||
.arg("-e")
|
||||
.arg(lib_file_path.trim_start_matches("squashfs-root/"))
|
||||
.arg("-no-progress")
|
||||
.arg(appimage_path)
|
||||
.arg(lib_file_path.trim_start_matches("squashfs-root/"))
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
|
||||
@@ -370,27 +370,51 @@ fn parse_elf32_sections(data: &[u8]) -> Option<String> {
|
||||
}
|
||||
|
||||
/// Fallback: run the AppImage with --appimage-updateinformation flag.
|
||||
/// Uses a 5-second timeout to avoid hanging on apps with custom AppRun scripts.
|
||||
fn extract_update_info_runtime(path: &Path) -> Option<String> {
|
||||
let output = std::process::Command::new(path)
|
||||
let mut child = std::process::Command::new(path)
|
||||
.arg("--appimage-updateinformation")
|
||||
.env("APPIMAGE_EXTRACT_AND_RUN", "1")
|
||||
.output()
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
.ok()?;
|
||||
|
||||
if output.status.success() {
|
||||
let info = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if !info.is_empty() && info.contains('|') {
|
||||
return Some(info);
|
||||
let timeout = std::time::Duration::from_secs(5);
|
||||
let start = std::time::Instant::now();
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
if status.success() {
|
||||
let mut output = String::new();
|
||||
if let Some(mut stdout) = child.stdout.take() {
|
||||
use std::io::Read;
|
||||
stdout.read_to_string(&mut output).ok()?;
|
||||
}
|
||||
let info = output.trim().to_string();
|
||||
if !info.is_empty() && info.contains('|') {
|
||||
return Some(info);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
Ok(None) => {
|
||||
if start.elapsed() >= timeout {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
log::warn!("Timed out reading update info from {}", path.display());
|
||||
return None;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// -- GitHub/GitLab API types for JSON deserialization --
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct GhRelease {
|
||||
tag_name: String,
|
||||
name: Option<String>,
|
||||
@@ -406,7 +430,6 @@ struct GhAsset {
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct GlRelease {
|
||||
tag_name: String,
|
||||
name: Option<String>,
|
||||
@@ -492,6 +515,12 @@ fn check_github_release(
|
||||
|
||||
let release: GhRelease = response.body_mut().read_json().ok()?;
|
||||
|
||||
log::info!(
|
||||
"GitHub release: tag={}, name={:?}",
|
||||
release.tag_name,
|
||||
release.name.as_deref().unwrap_or("(none)"),
|
||||
);
|
||||
|
||||
let latest_version = clean_version(&release.tag_name);
|
||||
|
||||
// Find matching asset using glob-like pattern
|
||||
@@ -549,6 +578,12 @@ fn check_gitlab_release(
|
||||
|
||||
let release: GlRelease = response.body_mut().read_json().ok()?;
|
||||
|
||||
log::info!(
|
||||
"GitLab release: tag={}, name={:?}",
|
||||
release.tag_name,
|
||||
release.name.as_deref().unwrap_or("(none)"),
|
||||
);
|
||||
|
||||
let latest_version = clean_version(&release.tag_name);
|
||||
|
||||
let download_url = release.assets.and_then(|assets| {
|
||||
@@ -669,18 +704,24 @@ fn glob_match(pattern: &str, text: &str) -> bool {
|
||||
|
||||
// Last part must match at the end (unless pattern ends with *)
|
||||
let last = parts[parts.len() - 1];
|
||||
if !last.is_empty() {
|
||||
let end_limit = if !last.is_empty() {
|
||||
if !text.ends_with(last) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
text.len() - last.len()
|
||||
} else {
|
||||
text.len()
|
||||
};
|
||||
|
||||
// Middle parts must appear in order
|
||||
// Middle parts must appear in order within the allowed range
|
||||
for part in &parts[1..parts.len() - 1] {
|
||||
if part.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(found) = text[pos..].find(part) {
|
||||
if pos >= end_limit {
|
||||
return false;
|
||||
}
|
||||
if let Some(found) = text[pos..end_limit].find(part) {
|
||||
pos += found + part.len();
|
||||
} else {
|
||||
return false;
|
||||
@@ -691,7 +732,7 @@ fn glob_match(pattern: &str, text: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Clean a version string - strip leading 'v' or 'V' prefix.
|
||||
fn clean_version(version: &str) -> String {
|
||||
pub(crate) fn clean_version(version: &str) -> String {
|
||||
let v = version.trim();
|
||||
v.strip_prefix('v')
|
||||
.or_else(|| v.strip_prefix('V'))
|
||||
@@ -949,24 +990,25 @@ fn download_file(
|
||||
|
||||
/// Verify that a file is a valid AppImage (has ELF header + AppImage magic bytes).
|
||||
fn verify_appimage(path: &Path) -> bool {
|
||||
if let Ok(data) = fs::read(path) {
|
||||
if data.len() < 12 {
|
||||
return false;
|
||||
}
|
||||
// Check ELF magic
|
||||
if &data[0..4] != b"\x7FELF" {
|
||||
return false;
|
||||
}
|
||||
// Check AppImage Type 2 magic at offset 8: AI\x02
|
||||
if data[8] == 0x41 && data[9] == 0x49 && data[10] == 0x02 {
|
||||
return true;
|
||||
}
|
||||
// Check AppImage Type 1 magic at offset 8: AI\x01
|
||||
if data[8] == 0x41 && data[9] == 0x49 && data[10] == 0x01 {
|
||||
return true;
|
||||
}
|
||||
use std::io::Read;
|
||||
let mut file = match fs::File::open(path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let mut header = [0u8; 12];
|
||||
if file.read_exact(&mut header).is_err() {
|
||||
return false;
|
||||
}
|
||||
false
|
||||
// Check ELF magic
|
||||
if &header[0..4] != b"\x7FELF" {
|
||||
return false;
|
||||
}
|
||||
// Check AppImage Type 2 magic at offset 8: AI\x02
|
||||
if header[8] == 0x41 && header[9] == 0x49 && header[10] == 0x02 {
|
||||
return true;
|
||||
}
|
||||
// Check AppImage Type 1 magic at offset 8: AI\x01
|
||||
header[8] == 0x41 && header[9] == 0x49 && header[10] == 0x01
|
||||
}
|
||||
|
||||
/// Perform an update using the best available method.
|
||||
|
||||
@@ -8,7 +8,7 @@ use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watche
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum WatchEvent {
|
||||
/// One or more AppImage files were created, modified, or deleted.
|
||||
Changed(#[allow(dead_code)] Vec<PathBuf>),
|
||||
Changed(Vec<PathBuf>),
|
||||
}
|
||||
|
||||
/// Start watching the given directories for AppImage file changes.
|
||||
|
||||
@@ -307,11 +307,9 @@ pub fn detect_desktop_environment() -> String {
|
||||
/// Result of analyzing a running process for Wayland usage.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RuntimeAnalysis {
|
||||
#[allow(dead_code)]
|
||||
pub pid: u32,
|
||||
pub has_wayland_socket: bool,
|
||||
pub has_x11_connection: bool,
|
||||
#[allow(dead_code)]
|
||||
pub env_vars: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
@@ -391,7 +389,6 @@ pub fn analyze_running_process(pid: u32) -> Result<RuntimeAnalysis, String> {
|
||||
has_wayland_socket = env_vars.iter().any(|(k, v)| {
|
||||
(k == "GDK_BACKEND" && v.contains("wayland"))
|
||||
|| (k == "QT_QPA_PLATFORM" && v.contains("wayland"))
|
||||
|| (k == "WAYLAND_DISPLAY" && !v.is_empty())
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user