From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 3C51270420 for ; Fri, 2 Apr 2021 13:21:04 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 33C83E31A for ; Fri, 2 Apr 2021 13:21:04 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 0CF8FE24F for ; Fri, 2 Apr 2021 13:20:58 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id CC016456D9 for ; Fri, 2 Apr 2021 13:20:57 +0200 (CEST) From: Fabian Ebner To: pbs-devel@lists.proxmox.com Date: Fri, 2 Apr 2021 13:20:45 +0200 Message-Id: <20210402112051.14628-5-f.ebner@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20210402112051.14628-1-f.ebner@proxmox.com> References: <20210402112051.14628-1-f.ebner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.007 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [check.rs, mod.rs, repositories.rs, types.rs] Subject: [pbs-devel] [PATCH v4 proxmox-apt 04/10] add check_repositories function X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 02 Apr 2021 11:21:04 -0000 which checks for bad suites and official URIs. Signed-off-by: Fabian Ebner --- Changes from v3: * switched to taking APTRepositoryFile slice as input * don't use vec! for a constant array * replace APTRepositoryWarning with a more general APTRepositoryInfo * warn about 'testing' when checking suites * warn about 'bullseye', but have it be a special warning so the UI can avoid showing it while a major upgrade happens * check for official host names and return a 'badge' info * add/adapt tests src/repositories/check.rs | 151 +++++++++++++++++++++- src/repositories/mod.rs | 19 ++- src/types.rs | 19 +++ tests/repositories.rs | 97 +++++++++++++- tests/sources.list.d.expected/bad.sources | 30 +++++ tests/sources.list.d/bad.sources | 29 +++++ 6 files changed, 341 insertions(+), 4 deletions(-) create mode 100644 tests/sources.list.d.expected/bad.sources create mode 100644 tests/sources.list.d/bad.sources diff --git a/src/repositories/check.rs b/src/repositories/check.rs index d0656cd..809b7bc 100644 --- a/src/repositories/check.rs +++ b/src/repositories/check.rs @@ -1,6 +1,22 @@ use anyhow::{bail, Error}; -use crate::types::{APTRepository, APTRepositoryFileType}; +use crate::types::{ + APTRepository, APTRepositoryFile, APTRepositoryFileType, APTRepositoryInfo, + APTRepositoryPackageType, +}; + +/// Checks if `suite` is some variant of `base_suite`, e.g. `buster-backports` +/// is a variant of `buster`. +fn suite_is_variant(suite: &str, base_suite: &str) -> bool { + matches!( + suite.strip_prefix(base_suite), + Some("") + | Some("-backports") + | Some("-backports-sloppy") + | Some("-updates") + | Some("/updates") + ) +} impl APTRepository { /// Makes sure that all basic properties of a repository are present and @@ -86,4 +102,137 @@ impl APTRepository { false } } + + /// Checks if old or unstable suites are configured and also that the + /// `stable` keyword is not used. + fn check_suites(&self, add_info: &mut dyn FnMut(String, String)) { + let old_suites = [ + "lenny", + "squeeze", + "wheezy", + "jessie", + "stretch", + "oldoldstable", + "oldstable", + ]; + + let next_suite = "bullseye"; + + let new_suites = ["testing", "unstable", "sid", "experimental"]; + + if self + .types + .iter() + .any(|package_type| *package_type == APTRepositoryPackageType::Deb) + { + for suite in self.suites.iter() { + if old_suites + .iter() + .any(|base_suite| suite_is_variant(suite, base_suite)) + { + add_info( + "warning".to_string(), + format!("old suite '{}' configured!", suite), + ); + } + + if suite_is_variant(suite, next_suite) { + add_info( + "ignore-pre-upgrade-warning".to_string(), + format!("suite '{}' should not be used in production!", suite), + ); + } + + if new_suites + .iter() + .any(|base_suite| suite_is_variant(suite, base_suite)) + { + add_info( + "warning".to_string(), + format!("suite '{}' should not be used in production!", suite), + ); + } + + if suite_is_variant(suite, "stable") { + add_info( + "warning".to_string(), + "use the name of the stable distribution instead of 'stable'!".to_string(), + ); + } + } + } + } + + /// Checks if an official host is configured in the repository. + fn check_uris(&self) -> Option<(String, String)> { + let official_host = |domains: &Vec<&str>| { + match domains.len() { + 3 => match domains[0] { + "ftp" | "deb" | "security" => domains[1] == "debian" && domains[2] == "org", + "download" | "enterprise" => domains[1] == "proxmox" && domains[2] == "com", + _ => false, + }, + // ftp.*.debian.org + 4 => domains[0] == "ftp" && domains[2] == "debian" && domains[3] == "org", + _ => false, + } + }; + + for uri in self.uris.iter() { + if let Some(begin) = uri.find("://") { + let mut host = uri.split_at(begin + 3).1; + if let Some(end) = host.find('/') { + host = host.split_at(end).0; + } // otherwise assume everything belongs is the host + + let domains = host.split('.').collect(); + + if official_host(&domains) { + return Some(("badge".to_string(), "official host name".to_string())); + } + } + } + + None + } +} + +impl APTRepositoryFile { + /// Checks if old or unstable suites are configured and also that the + /// `stable` keyword is not used. + pub fn check_suites(&self) -> Vec { + let mut infos = vec![]; + + for (n, repo) in self.repositories.iter().enumerate() { + let mut add_info = |kind, message| { + infos.push(APTRepositoryInfo { + path: self.path.clone(), + number: n + 1, + kind, + message, + }) + }; + repo.check_suites(&mut add_info); + } + + infos + } + + /// Checks for official URIs. + pub fn check_uris(&self) -> Vec { + let mut infos = vec![]; + + for (n, repo) in self.repositories.iter().enumerate() { + if let Some((kind, message)) = repo.check_uris() { + infos.push(APTRepositoryInfo { + path: self.path.clone(), + number: n + 1, + kind, + message, + }); + } + } + + infos + } } diff --git a/src/repositories/mod.rs b/src/repositories/mod.rs index b7919a9..c2bbc06 100644 --- a/src/repositories/mod.rs +++ b/src/repositories/mod.rs @@ -4,7 +4,7 @@ use anyhow::{bail, format_err, Error}; use crate::types::{ APTRepository, APTRepositoryFile, APTRepositoryFileError, APTRepositoryFileType, - APTRepositoryOption, + APTRepositoryInfo, APTRepositoryOption, }; mod list_parser; @@ -148,6 +148,23 @@ impl APTRepositoryFile { } } +/// Provides additional information about the repositories. +/// +/// The kind of information can be: +/// `warnings` for bad suites. +/// `ignore-pre-upgrade-warning` when the next stable suite is configured. +/// `badge` for official URIs. +pub fn check_repositories(files: &[APTRepositoryFile]) -> Vec { + let mut infos = vec![]; + + for file in files.iter() { + infos.append(&mut file.check_suites()); + infos.append(&mut file.check_uris()); + } + + infos +} + /// Checks if the enterprise repository for the specified Proxmox product is /// configured and enabled. pub fn enterprise_repository_enabled(files: &[APTRepositoryFile], product: &str) -> bool { diff --git a/src/types.rs b/src/types.rs index 45b8455..94b6411 100644 --- a/src/types.rs +++ b/src/types.rs @@ -244,3 +244,22 @@ impl std::error::Error for APTRepositoryFileError { None } } + +#[api] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +/// Additional information for a repository. +pub struct APTRepositoryInfo { + /// Path to the defining file. + #[serde(skip_serializing_if = "String::is_empty")] + pub path: String, + + /// Number of the associated respository within the file. + pub number: usize, + + /// Info kind (e.g. "warning") + pub kind: String, + + /// Info message + pub message: String, +} diff --git a/tests/repositories.rs b/tests/repositories.rs index d23beac..2b9b208 100644 --- a/tests/repositories.rs +++ b/tests/repositories.rs @@ -3,9 +3,10 @@ use std::path::PathBuf; use anyhow::{bail, format_err, Error}; use proxmox_apt::repositories::{ - enterprise_repository_enabled, no_subscription_repository_enabled, write_repositories, + check_repositories, enterprise_repository_enabled, no_subscription_repository_enabled, + write_repositories, }; -use proxmox_apt::types::APTRepositoryFile; +use proxmox_apt::types::{APTRepositoryFile, APTRepositoryInfo}; #[test] fn test_parse_write() -> Result<(), Error> { @@ -161,3 +162,95 @@ fn test_proxmox_repositories() -> Result<(), Error> { Ok(()) } + +#[test] +fn test_check_repositories() -> Result<(), Error> { + let test_dir = std::env::current_dir()?.join("tests"); + let read_dir = test_dir.join("sources.list.d"); + + let absolute_suite_list = read_dir.join("absolute_suite.list"); + let mut file = APTRepositoryFile::new(&absolute_suite_list)?.unwrap(); + file.parse()?; + + let infos = check_repositories(&vec![file]); + + assert_eq!(infos.is_empty(), true); + let pve_list = read_dir.join("pve.list"); + let mut file = APTRepositoryFile::new(&pve_list)?.unwrap(); + file.parse()?; + + let path_string = pve_list.into_os_string().into_string().unwrap(); + + let mut expected_infos = vec![]; + for n in 1..=5 { + expected_infos.push(APTRepositoryInfo { + path: path_string.clone(), + number: n, + kind: "badge".to_string(), + message: "official host name".to_string(), + }); + } + + let mut infos = check_repositories(&vec![file]); + + assert_eq!(infos.sort(), expected_infos.sort()); + + let bad_sources = read_dir.join("bad.sources"); + let mut file = APTRepositoryFile::new(&bad_sources)?.unwrap(); + file.parse()?; + + let path_string = bad_sources.into_os_string().into_string().unwrap(); + + let mut expected_infos = vec![ + APTRepositoryInfo { + path: path_string.clone(), + number: 1, + kind: "warning".to_string(), + message: "suite 'sid' should not be used in production!".to_string(), + }, + APTRepositoryInfo { + path: path_string.clone(), + number: 2, + kind: "warning".to_string(), + message: "old suite 'lenny-backports' configured!".to_string(), + }, + APTRepositoryInfo { + path: path_string.clone(), + number: 3, + kind: "warning".to_string(), + message: "old suite 'stretch/updates' configured!".to_string(), + }, + APTRepositoryInfo { + path: path_string.clone(), + number: 4, + kind: "warning".to_string(), + message: "use the name of the stable distribution instead of 'stable'!".to_string(), + }, + APTRepositoryInfo { + path: path_string.clone(), + number: 5, + kind: "ignore-pre-upgrade-warning".to_string(), + message: "suite 'bullseye' should not be used in production!".to_string(), + }, + APTRepositoryInfo { + path: path_string.clone(), + number: 6, + kind: "warning".to_string(), + message: "suite 'testing' should not be used in production!".to_string(), + }, + ]; + for n in 1..=6 { + expected_infos.push(APTRepositoryInfo { + path: path_string.clone(), + number: n, + kind: "badge".to_string(), + message: "official URI".to_string(), + }); + } + + let mut infos = check_repositories(&vec![file]); + + assert_eq!(infos.sort(), expected_infos.sort()); + + Ok(()) +} diff --git a/tests/sources.list.d.expected/bad.sources b/tests/sources.list.d.expected/bad.sources new file mode 100644 index 0000000..36ff7a0 --- /dev/null +++ b/tests/sources.list.d.expected/bad.sources @@ -0,0 +1,30 @@ +Types: deb +URIs: http://ftp.at.debian.org/debian +Suites: sid +Components: main contrib + +Types: deb +URIs: http://ftp.at.debian.org/debian +Suites: lenny-backports +Components: contrib + +Types: deb +URIs: http://security.debian.org +Suites: stretch/updates +Components: main contrib + +Types: deb +URIs: http://ftp.at.debian.org/debian +Suites: stable +Components: main + +Types: deb +URIs: http://ftp.at.debian.org/debian +Suites: bullseye +Components: main + +Types: deb +URIs: http://ftp.at.debian.org/debian +Suites: testing +Components: main + diff --git a/tests/sources.list.d/bad.sources b/tests/sources.list.d/bad.sources new file mode 100644 index 0000000..6f2524a --- /dev/null +++ b/tests/sources.list.d/bad.sources @@ -0,0 +1,29 @@ +Types: deb +URIs: http://ftp.at.debian.org/debian +Suites: sid +Components: main contrib + +Types: deb +URIs: http://ftp.at.debian.org/debian +Suites: lenny-backports +Components: contrib + +Types: deb +URIs: http://security.debian.org +Suites: stretch/updates +Components: main contrib + +Suites: stable +URIs: http://ftp.at.debian.org/debian +Components: main +Types: deb + +Suites: bullseye +URIs: http://ftp.at.debian.org/debian +Components: main +Types: deb + +Suites: testing +URIs: http://ftp.at.debian.org/debian +Components: main +Types: deb -- 2.20.1