diff --git a/hipcheck/dist.toml b/hipcheck/dist.toml index 5e311e2d..6fe20fab 100644 --- a/hipcheck/dist.toml +++ b/hipcheck/dist.toml @@ -11,9 +11,6 @@ installers = ["shell", "powershell"] # Whether to install an updater program install-updater = true -# Make sure to include the configuration. -include = ["../config/"] - # Make sure that both Hipcheck and all the plugins are built with the protobuf # compiler present on their platform. diff --git a/hipcheck/src/cli.rs b/hipcheck/src/cli.rs index fab5e4a7..c4edd792 100644 --- a/hipcheck/src/cli.rs +++ b/hipcheck/src/cli.rs @@ -368,7 +368,7 @@ fn hc_env_var_value_enum(name: &'static str) -> Option { pub enum FullCommands { Check(CheckArgs), Schema(SchemaArgs), - Setup(SetupArgs), + Setup, Ready, Update(UpdateArgs), Cache(CacheArgs), @@ -383,7 +383,7 @@ impl From<&Commands> for FullCommands { match command { Commands::Check(args) => FullCommands::Check(args.clone()), Commands::Schema(args) => FullCommands::Schema(args.clone()), - Commands::Setup(args) => FullCommands::Setup(args.clone()), + Commands::Setup => FullCommands::Setup, Commands::Ready => FullCommands::Ready, Commands::Scoring => FullCommands::Scoring, Commands::Update(args) => FullCommands::Update(args.clone()), @@ -400,15 +400,7 @@ pub enum Commands { /// Print the JSON schema for output of a specific `check` command. Schema(SchemaArgs), /// Initialize Hipcheck config file and script file locations. - /// - /// The "destination" directories for configuration files - /// Hipcheck needs are determined with the following methods, in - /// increasing precedence: - /// - /// 1. Platform-specific defaults - /// 2. `HC_CONFIG` environment variable - /// 3. `--config` command line flag - Setup(SetupArgs), + Setup, /// Check if Hipcheck is ready to run. Ready, /// Print the tree used to weight analyses during scoring. @@ -734,16 +726,6 @@ pub enum SchemaCommand { Repo, } -#[derive(Debug, Clone, clap::Args)] -pub struct SetupArgs { - /// Do not use the network to download setup files. - #[clap(short = 'o', long)] - pub offline: bool, - /// Path to local Hipcheck release archive or directory. - #[clap(short = 's', long)] - pub source: Option, -} - #[derive(Debug, Clone, clap::Args)] pub struct UpdateArgs { /// Installs the specified tag instead of the latest version diff --git a/hipcheck/src/main.rs b/hipcheck/src/main.rs index 46fe020d..1d3c945e 100644 --- a/hipcheck/src/main.rs +++ b/hipcheck/src/main.rs @@ -33,12 +33,12 @@ use crate::{ report::report_builder::{build_report, Report}, score::score_results, session::Session, - setup::{resolve_and_transform_source, SourceType}, + setup::write_config_binaries, shell::Shell, }; use cli::{ CacheArgs, CacheOp, CheckArgs, CliConfig, FullCommands, PluginArgs, SchemaArgs, SchemaCommand, - SetupArgs, UpdateArgs, + UpdateArgs, }; use config::AnalysisTreeNode; use core::fmt; @@ -89,7 +89,7 @@ fn main() -> ExitCode { match config.subcommand() { Some(FullCommands::Check(args)) => return cmd_check(&args, &config), Some(FullCommands::Schema(args)) => cmd_schema(&args), - Some(FullCommands::Setup(args)) => return cmd_setup(&args, &config), + Some(FullCommands::Setup) => return cmd_setup(&config), Some(FullCommands::Ready) => cmd_ready(&config), Some(FullCommands::Update(args)) => cmd_update(&args), Some(FullCommands::Cache(args)) => return cmd_cache(args, &config), @@ -247,61 +247,7 @@ fn cmd_print_weights(config: &CliConfig) -> Result<()> { Ok(()) } -/// Copy individual files in dir instead of entire dir, to avoid users accidentally -/// overwriting important dirs such as /usr/bin/ -fn copy_dir_contents, Q: AsRef>(from: P, to: Q) -> Result<()> { - fn inner(from: &Path, to: &Path) -> Result<()> { - let src = from.to_path_buf(); - if !src.is_dir() { - return Err(hc_error!("source path must be a directory")); - } - let dst: PathBuf = to.to_path_buf(); - if !dst.is_dir() { - return Err(hc_error!("target path must be a directory")); - } - - for entry in walkdir::WalkDir::new(&src) { - let src_f_path = entry?.path().to_path_buf(); - if src_f_path == src { - continue; - } - let mut dst_f_path = dst.clone(); - dst_f_path.push( - src_f_path - .file_name() - .ok_or(hc_error!("src dir entry without file name"))?, - ); - // This is ok for now because we only copy files, no dirs - std::fs::copy(src_f_path, dst_f_path)?; - } - Ok(()) - } - inner(from.as_ref(), to.as_ref()) -} - -fn cmd_setup(args: &SetupArgs, config: &CliConfig) -> ExitCode { - // Find or download a Hipcheck bundle source and decompress - let source = match resolve_and_transform_source(args) { - Err(e) => { - Shell::print_error(&e, Format::Human); - return ExitCode::FAILURE; - } - Ok(x) => x, - }; - - // Derive the config path from the source path - let src_conf_path = match &source.path { - SourceType::Dir(p) => pathbuf![p.as_path(), "config"], - _ => { - Shell::print_error( - &hc_error!("expected source to be a directory"), - Format::Human, - ); - source.cleanup(); - return ExitCode::FAILURE; - } - }; - +fn cmd_setup(config: &CliConfig) -> ExitCode { // Make config dir if not exist let Some(tgt_conf_path) = config.config() else { Shell::print_error(&hc_error!("target config dir not specified"), Format::Human); @@ -323,10 +269,10 @@ fn cmd_setup(args: &SetupArgs, config: &CliConfig) -> ExitCode { return ExitCode::FAILURE; }; - // Copy local config/data dirs to target locations - if let Err(e) = copy_dir_contents(src_conf_path, &abs_conf_path) { + // Write config file binaries to target directory + if let Err(e) = write_config_binaries(tgt_conf_path) { Shell::print_error( - &hc_error!("failed to copy config dir contents: {}", e), + &hc_error!("failed to write config binaries to config dir {}", e), Format::Human, ); return ExitCode::FAILURE; @@ -349,8 +295,6 @@ fn cmd_setup(args: &SetupArgs, config: &CliConfig) -> ExitCode { println!("Run `hc help` to get started"); - source.cleanup(); - ExitCode::SUCCESS } diff --git a/hipcheck/src/setup.rs b/hipcheck/src/setup.rs index c003ef50..47b492d9 100644 --- a/hipcheck/src/setup.rs +++ b/hipcheck/src/setup.rs @@ -1,182 +1,35 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::{cli::SetupArgs, error::Result, hc_error, util::http::agent}; -use regex::Regex; -use std::{ - fs::File, - path::{Path, PathBuf}, - sync::OnceLock, -}; -use tar::Archive; -use xz2::read::XzDecoder; - -static R_HC_SOURCE: OnceLock = OnceLock::new(); - -fn get_source_regex<'a>() -> &'a Regex { - R_HC_SOURCE.get_or_init(|| Regex::new("^hipcheck-[a-z0-9_]+-[a-z0-9_]+-[a-z0-9_]+").unwrap()) -} - -#[derive(Debug, Clone)] -pub struct SetupSourcePath { - pub path: SourceType, - delete: bool, -} -impl SetupSourcePath { - pub fn cleanup(&self) { - use SourceType::*; - if self.delete { - let _res = match &self.path { - Dir(p) => std::fs::remove_dir_all(p), - Tar(p) | Zip(p) => std::fs::remove_file(p), - }; - } - } - // Convert to a SourceType::Dir - pub fn try_unpack(self) -> Result { - use SourceType::*; - let (new_path, delete) = match self.path.clone() { - Dir(p) => (p, self.delete), - // For tars and zips, we have to provide the decompressor with the parent dir, - // which will produce a directory with the same name as the archive minus the - // file extension. We return the name of that new directory. - Tar(p) => { - let new_fname: &str = p - .file_name() - .ok_or(hc_error!("malformed file name"))? - .to_str() - .ok_or(hc_error!("failed to convert tar file name to utf8"))? - .strip_suffix(".tar.xz") - .ok_or(hc_error!("tar file with improper extension"))?; - let tgt_p = p.with_file_name(new_fname); - let parent_p = PathBuf::from(tgt_p.as_path().parent().unwrap()); - let tar_gz = File::open(p)?; - let mut archive = Archive::new(XzDecoder::new(tar_gz)); - archive.unpack(parent_p.as_path())?; - self.cleanup(); - (tgt_p, true) - } - Zip(p) => { - let new_fname: &str = p - .file_stem() - .ok_or(hc_error!("malformed .zip file name"))? - .to_str() - .ok_or(hc_error!(".zip file not utf8"))?; - let tgt_p = p.with_file_name(new_fname); - let parent_p = PathBuf::from(tgt_p.as_path().parent().unwrap()); - let mut archive = zip::ZipArchive::new(File::open(p)?)?; - archive.extract(parent_p.as_path())?; - self.cleanup(); - (tgt_p, true) - } - }; - Ok(SetupSourcePath { - path: SourceType::Dir(new_path), - delete, - }) - } -} - -#[derive(Debug, Clone)] -pub enum SourceType { - Dir(PathBuf), - Tar(PathBuf), - Zip(PathBuf), -} -impl TryFrom<&Path> for SourceType { - type Error = crate::error::Error; - fn try_from(value: &Path) -> Result { - use SourceType::*; - let source_regex = get_source_regex(); - let file_name = value - .file_name() - .ok_or(hc_error!("path without a file name"))? - .to_str() - .ok_or(hc_error!("file name not valid utf8"))?; - if !source_regex.is_match(file_name) { - return Err(hc_error!("file does not match regex")); - } - if value.is_dir() { - return Ok(Dir(PathBuf::from(value))); - } - if file_name.ends_with(".tar.xz") { - return Ok(Tar(PathBuf::from(value))); - } - if file_name.ends_with(".zip") { - return Ok(Zip(PathBuf::from(value))); - } - Err(hc_error!("unknown source type")) - } -} - -pub fn search_dir_for_source(path: &Path) -> Option { - for entry in walkdir::WalkDir::new(path) - .max_depth(1) - .into_iter() - .flatten() - { - if let Ok(source) = SourceType::try_from(entry.path()) { - return Some(source); - } - } - None -} - -pub fn try_get_source_path_from_path(path: &Path) -> Option { - // First treat path as a direct source dir / archive - if let Ok(source) = SourceType::try_from(path) { - Some(source) - } - // If that failed and path is a dir, see if we can find it underneath - else if path.is_dir() { - search_dir_for_source(path) - } else { - None - } -} - -// Search for a dir or archive matching the format of our `cargo-dist` release bundle. Search -// hierarchy is 1) source cmdline arg, 2) current dir 3) platform downloads dir. If nothing -// found and user did not forbid internet access, we can then pull from github release page. -pub fn try_resolve_source_path(args: &SetupArgs) -> Result { - let try_dirs: Vec> = vec![ - args.source.clone(), - std::env::current_dir().ok(), - dirs::download_dir(), +use crate::error::Result; +use std::io::Write; +use std::{fs::File, path::Path}; + +static BINARY_TOML: &str = include_str!("../../config/Binary.toml"); +static EXEC_KDL: &str = include_str!("../../config/Exec.kdl"); +static HIPCHECK_KDL: &str = include_str!("../../config/Hipcheck.kdl"); +static HIPCHECK_TOML: &str = include_str!("../../config/Hipcheck.toml"); +static LANGS_TOML: &str = include_str!("../../config/Langs.toml"); +static ORGS_KDL: &str = include_str!("../../config/Orgs.kdl"); +static TYPOS_TOML: &str = include_str!("../../config/Typos.toml"); + +pub fn write_config_binaries(path: &Path) -> Result<()> { + std::fs::create_dir_all(path)?; + + let files = [ + ("Langs.toml", LANGS_TOML), + ("Typos.toml", TYPOS_TOML), + ("Binary.toml", BINARY_TOML), + ("Exec.kdl", EXEC_KDL), + ("Hipcheck.kdl", HIPCHECK_KDL), + ("Hipcheck.toml", HIPCHECK_TOML), + ("Orgs.kdl", ORGS_KDL), ]; - for try_dir in try_dirs.into_iter().flatten() { - if let Some(bp) = try_get_source_path_from_path(try_dir.as_path()) { - return Ok(SetupSourcePath { - path: bp, - delete: false, - }); - } - } - // If allowed by user, download from github - if !args.offline { - // Since we're just getting the conf/target dir from here, we don't - // technically need to grab the right version - let f_name: &str = "hipcheck-x86_64-unknown-linux-gnu.tar.xz"; - let remote = format!( - "https://github.com/mitre/hipcheck/releases/download/hipcheck-v{}/{}", - env!("CARGO_PKG_VERSION"), - f_name - ); - let mut out_file = File::create(f_name)?; - let agent = agent::agent(); - - println!("Downloading Hipcheck release from remote."); - let resp = agent.get(remote.as_str()).call()?; - std::io::copy(&mut resp.into_reader(), &mut out_file)?; - - return Ok(SetupSourcePath { - path: SourceType::Tar(std::fs::canonicalize(f_name)?), - delete: true, - }); + for (file_name, content) in &files { + let file_path = path.join(file_name); + let mut file = File::create(file_path)?; + file.write_all(content.as_bytes())?; } - Err(hc_error!("could not find suitable source file")) -} -pub fn resolve_and_transform_source(args: &SetupArgs) -> Result { - try_resolve_source_path(args)?.try_unpack() + Ok(()) } diff --git a/site/content/docs/contributing/developer-docs/repo-structure.md b/site/content/docs/contributing/developer-docs/repo-structure.md index 981d1931..ae96a967 100644 --- a/site/content/docs/contributing/developer-docs/repo-structure.md +++ b/site/content/docs/contributing/developer-docs/repo-structure.md @@ -52,7 +52,7 @@ Important modules within the `hipcheck/` binary crate include: - `session/` - Managing a given Hipcheck `check` execution from start to finish, including plugin retrieval and execution, policy file parsing, analysis, scoring, and report building. -- `setup.rs` - Implements the `hc setup` subcommand that does one-time actions +- `setup.rs` - Implements the `hc setup` subcommand that does one-time config file setup as part of a Hipcheck installation. - `shell/` - Managing the terminal output of the Hipcheck `hc` process. - `source/` - Code for manipulating Git repositories. diff --git a/site/content/docs/guide/cli/hc-setup.md b/site/content/docs/guide/cli/hc-setup.md index 8664b6a3..b134ce46 100644 --- a/site/content/docs/guide/cli/hc-setup.md +++ b/site/content/docs/guide/cli/hc-setup.md @@ -11,23 +11,11 @@ and again after updating Hipcheck, to ensure you have the required configuration and data files needed for Hipcheck to run. When installing Hipcheck, regardless of method, you are only installing the -`hc` binary, not these additional files. `hc setup` gathers those files for you, -and installs them into the appropriate locations. +`hc` binary, not these additional configuration files. `hc setup` identifies +the correct location in your system for configuration files and writes the +files to that directory. -If Hipcheck has been installed with the recommended install scripts included -with each release, then the correct configuration and data files for each -version are included with the bundle downloaded by that script. In that case, -`hc setup` will attempt to find those files locally and copy them into the -configuration and data directories. +It also produces an export command that should be used to set the `HC_CONFIG` +environmental variable to the relevant directory, as necessary to run `hc check`. -If Hipcheck was installed via another method, or the files can't be found, -then `hc setup` will attempt to download them from the appropriate Hipcheck -release. Users can pass the `-o`/`--offline` flag to ensure `hc setup` does -_not_ use the network to download materials, in which case `hc setup` will -fail if the files can't be found locally. - -The installation directories for the configuration and data files are -specified in the way they're normally specified. For more information, -see the documentation on Hipcheck's [Path Flags](@/docs/guide/cli/general-flags.md#path-flags). - -`hc setup` also supports Hipcheck's [General Flags](@/docs/guide/cli/general-flags.md). +`hc setup` supports Hipcheck's [General Flags](@/docs/guide/cli/general-flags.md). diff --git a/site/content/docs/guide/cli/hc-update.md b/site/content/docs/guide/cli/hc-update.md index 3b0d7f60..647323e1 100644 --- a/site/content/docs/guide/cli/hc-update.md +++ b/site/content/docs/guide/cli/hc-update.md @@ -18,7 +18,7 @@ or `hipcheck-update`, due to historic bugs). The `hc update` command simply delegates to this separate update program, and provides the same interface that this separate update program does. In general, you only need to run `hc update` with no arguments, followed -by `hc setup` to ensure you have the latest configuration and data files. +by `hc setup` to ensure your configuration and data files are setup. If you want to specifically download a version besides the most recent version of Hipcheck, you can use the following flags: