Skip to content

Commit

Permalink
fix(setup): write configuration files directly from memory
Browse files Browse the repository at this point in the history
During hc setup write config files from memory, without local dirs or remote source

Signed-off-by: Kirill Usubyan <[email protected]>
  • Loading branch information
KirilldogU committed Jan 27, 2025
1 parent c5d0755 commit 0fc219a
Show file tree
Hide file tree
Showing 7 changed files with 46 additions and 282 deletions.
3 changes: 0 additions & 3 deletions hipcheck/dist.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
24 changes: 3 additions & 21 deletions hipcheck/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ fn hc_env_var_value_enum<E: ValueEnum>(name: &'static str) -> Option<E> {
pub enum FullCommands {
Check(CheckArgs),
Schema(SchemaArgs),
Setup(SetupArgs),
Setup,
Ready,
Update(UpdateArgs),
Cache(CacheArgs),
Expand All @@ -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()),
Expand All @@ -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.
Expand Down Expand Up @@ -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<PathBuf>,
}

#[derive(Debug, Clone, clap::Args)]
pub struct UpdateArgs {
/// Installs the specified tag instead of the latest version
Expand Down
70 changes: 7 additions & 63 deletions hipcheck/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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<P: AsRef<Path>, Q: AsRef<Path>>(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);
Expand All @@ -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;
Expand All @@ -349,8 +295,6 @@ fn cmd_setup(args: &SetupArgs, config: &CliConfig) -> ExitCode {

println!("Run `hc help` to get started");

source.cleanup();

ExitCode::SUCCESS
}

Expand Down
203 changes: 28 additions & 175 deletions hipcheck/src/setup.rs
Original file line number Diff line number Diff line change
@@ -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<Regex> = 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<SetupSourcePath> {
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<SourceType> {
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<SourceType> {
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<SourceType> {
// 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<SetupSourcePath> {
let try_dirs: Vec<Option<PathBuf>> = 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<SetupSourcePath> {
try_resolve_source_path(args)?.try_unpack()
Ok(())
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 0fc219a

Please sign in to comment.