Skip to content

Commit

Permalink
feat: improve experience when using local plugins
Browse files Browse the repository at this point in the history
This change set improves the ergonomics of run 'local' plugins. Rather
than running them at the path reachable in a provided Policy file,
hipcheck now copies the entrypoints and plugin manifest files to the
same cache locations that downloaded plugins would use.
  • Loading branch information
patrickjcasey authored and j-lanson committed Oct 31, 2024
1 parent 592764e commit 56dd7a7
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 15 deletions.
6 changes: 3 additions & 3 deletions config/Hipcheck.kdl
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
plugins {
plugin "mitre/activity" version="0.1.0"
plugin "mitre/binary" version="0.1.0"
plugin "mitre/binary" version="0.1.0" manifest="./plugins/binary/plugin.kdl"
plugin "mitre/fuzz" version="0.1.0" manifest="./plugins/fuzz/plugin.kdl"
plugin "mitre/review" version="0.1.0" manifest="./plugins/review/plugin.kdl"
plugin "mitre/typo" version="0.1.0" manifest="./plugins/typo/plugin.kdl"
plugin "mitre/affiliation" version="0.1.0"
plugin "mitre/entropy" version="0.1.0"
plugin "mitre/churn" version="0.1.0"
plugin "mitre/churn" version="0.1.0" manifest="./plugins/churn/plugin.kdl"
}
patch {
plugin "mitre/github_api" {
Expand All @@ -24,7 +24,7 @@ analyze {
binary-file-threshold 0
}
analysis "mitre/fuzz" policy="(eq #t $)"
analysis "mitre/review" policy="(lte $ 0.05)"
analysis "mitre/review" policy="(lte (divz (count (filter (eq #f) $)) (count $)) 0.05)"
}

category "attacks" {
Expand Down
142 changes: 142 additions & 0 deletions hipcheck/src/plugin/plugin_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use std::{

#[cfg(test)]
use crate::plugin::arch::KnownArch;
use crate::util::kdl::ToKdlNode;

// NOTE: the implementation in this crate was largely derived from RFD #4

Expand Down Expand Up @@ -55,6 +56,22 @@ impl Entrypoints {
}
}

impl ToKdlNode for Entrypoints {
fn to_kdl_node(&self) -> KdlNode {
let mut entrypoint_parent = KdlNode::new("entrypoint");
let mut entrypoint_children = KdlDocument::new();
let entrypoint_children_nodes = entrypoint_children.nodes_mut();
for (arch, entrypoint) in self.0.iter() {
let mut entry = KdlNode::new("on");
entry.insert("arch", arch.to_string());
entry.insert(0, entrypoint.to_owned());
entrypoint_children_nodes.push(entry);
}
entrypoint_parent.set_children(entrypoint_children);
entrypoint_parent
}
}

impl ParseKdlNode for Entrypoints {
fn kdl_key() -> &'static str {
"entrypoint"
Expand Down Expand Up @@ -163,6 +180,25 @@ impl PluginDependencyList {
}
}

impl ToKdlNode for PluginDependencyList {
fn to_kdl_node(&self) -> KdlNode {
let mut dependency_parent = KdlNode::new("dependencies");
let mut dependency_children = KdlDocument::new();
let dependency_children_nodes = dependency_children.nodes_mut();
for dep in self.0.iter() {
let mut entry = KdlNode::new("plugin");
entry.insert(0, dep.plugin_id.to_policy_file_plugin_identifier());
entry.insert("version", dep.plugin_id.version().0.as_str());
if let Some(manifest) = &dep.manifest {
entry.insert("manifest", manifest.to_string());
}
dependency_children_nodes.push(entry);
}
dependency_parent.set_children(dependency_children);
dependency_parent
}
}

impl ParseKdlNode for PluginDependencyList {
fn kdl_key() -> &'static str {
"dependencies"
Expand Down Expand Up @@ -200,12 +236,63 @@ impl PluginManifest {
self.entrypoints.0.get(arch).cloned()
}

fn set_entrypoint(&mut self, arch: Arch, entrypoint: String) {
self.entrypoints.0.insert(arch, entrypoint);
}

pub fn from_file<P>(path: P) -> Result<Self, Error>
where
P: AsRef<Path>,
{
Self::from_str(read_string(path)?.as_str())
}

/// Update the directory that an entrypoint is stored in
///
/// Returns previous entrypoint
pub fn update_entrypoint<P>(&mut self, arch: &Arch, new_directory: P) -> Result<PathBuf, Error>
where
P: AsRef<Path>,
{
let current_entrypoint = self
.entrypoints
.0
.remove(arch)
.map(PathBuf::from)
.ok_or(hc_error!("No entrypoint for current arch ({})", arch))?;

fn new_path(new_directory: &Path, current_entrypoint: &Path) -> Result<PathBuf, Error> {
let entrypoint_filename =
new_directory.join(current_entrypoint.file_name().ok_or(hc_error!(
"'{}' entrypoint does not contain a valid filename",
current_entrypoint.to_string_lossy()
))?);
Ok(entrypoint_filename)
}

let new_entrypoint = new_path(new_directory.as_ref(), &current_entrypoint)?;
self.set_entrypoint(arch.clone(), new_entrypoint.to_string_lossy().to_string());
Ok(current_entrypoint)
}

/// convert a `PluginManifest` to a `KdlDocument`
fn to_kdl(&self) -> KdlDocument {
let mut document = KdlDocument::new();
document.nodes_mut().extend([
self.publisher.to_kdl_node(),
self.name.to_kdl_node(),
self.version.to_kdl_node(),
self.license.to_kdl_node(),
self.entrypoints.to_kdl_node(),
self.dependencies.to_kdl_node(),
]);
document
}

/// convert `PluginManifest` to a KDL-formatted String
pub fn to_kdl_formatted_string(&self) -> String {
self.to_kdl().to_string()
}
}

impl FromStr for PluginManifest {
Expand Down Expand Up @@ -482,4 +569,59 @@ dependencies {
};
assert_eq!(plugin_manifest, expected_manifest);
}

#[test]
fn test_to_kdl() {
let mut entrypoints = Entrypoints::new();
entrypoints
.insert(
Arch::Known(KnownArch::Aarch64AppleDarwin),
"./target/debug/activity_sdk".to_owned(),
)
.unwrap();
entrypoints
.insert(
Arch::Known(KnownArch::X86_64AppleDarwin),
"./target/debug/activity_sdk".to_owned(),
)
.unwrap();
entrypoints
.insert(
Arch::Known(KnownArch::X86_64UnknownLinuxGnu),
"./target/debug/activity_sdk".to_owned(),
)
.unwrap();
entrypoints
.insert(
Arch::Known(KnownArch::X86_64PcWindowsMsvc),
"./target/debug/activity_sdk".to_owned(),
)
.unwrap();

let mut dependencies = PluginDependencyList::new();
dependencies.push(PluginDependency::new(
PluginId::new(
PluginPublisher::new("mitre".to_owned()),
PluginName::new("git".to_owned()),
PluginVersion::new("0.1.0".to_owned()),
),
Some(ManifestLocation::Local("./plugins/git/plugin.kdl".into())),
));

let plugin_manifest = PluginManifest {
publisher: PluginPublisher::new("mitre".to_owned()),
name: PluginName::new("activity".to_owned()),
version: PluginVersion::new("0.1.0".to_owned()),
license: License::new("Apache-2.0".to_owned()),
entrypoints,
dependencies,
};

let plugin_manifest_string = plugin_manifest.to_kdl_formatted_string();

assert_eq!(
plugin_manifest,
PluginManifest::from_str(&plugin_manifest_string).unwrap()
)
}
}
30 changes: 22 additions & 8 deletions hipcheck/src/plugin/retrieval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
util::http::agent::agent,
};
use flate2::read::GzDecoder;
use fs_extra::dir::remove;
use fs_extra::{dir::remove, file::write_all};
use std::{
collections::HashSet,
fs::File,
Expand Down Expand Up @@ -115,33 +115,47 @@ fn retrieve_plugin_from_network(
))
}

/// retrieves a plugin from the local filesystem
/// retrieves a plugin from the local filesystem by copying its `plugin.kdl` and `entrypoint` binary to the plugin_cache
fn retrieve_local_plugin(
plugin_id: PluginId,
plugin_manifest_path: &PathBuf,
plugin_cache: &HcPluginCache,
) -> Result<PluginManifest, Error> {
let download_dir = plugin_cache.plugin_download_dir(&plugin_id);
std::fs::create_dir_all(download_dir.as_path()).map_err(|e| {
std::fs::create_dir_all(&download_dir).map_err(|e| {
hc_error!(
"Error [{}] creating download directory {}",
e,
download_dir.to_string_lossy()
)
})?;
let plugin_kdl_path = plugin_cache.plugin_kdl(&plugin_id);

let mut plugin_manifest = PluginManifest::from_file(plugin_manifest_path)?;
let current_arch = get_current_arch();

let original_entrypoint = plugin_manifest
.update_entrypoint(&current_arch, plugin_cache.plugin_download_dir(&plugin_id))?;

// @Note - sneaky potential for unexpected behavior if we write local plugin manifest
// to a cache dir that already included a remote download
std::fs::copy(plugin_manifest_path, &plugin_kdl_path).map_err(|e| {
std::fs::copy(
&original_entrypoint,
plugin_cache
.plugin_download_dir(&plugin_id)
// unwrap is safe here, we just updated the entrypoint for current arch
.join(plugin_manifest.get_entrypoint(&current_arch).unwrap()),
)?;

let plugin_kdl_path = plugin_cache.plugin_kdl(&plugin_id);
write_all(&plugin_kdl_path, &plugin_manifest.to_kdl_formatted_string()).map_err(|e| {
hc_error!(
"Error [{}] copying local plugin manifest for {} to cache",
"Error [{}] writing {}",
e,
plugin_id.to_policy_file_plugin_identifier()
plugin_kdl_path.to_string_lossy()
)
})?;

PluginManifest::from_file(plugin_kdl_path)
Ok(plugin_manifest)
}

/// This function does the following:
Expand Down
11 changes: 10 additions & 1 deletion hipcheck/src/policy/policy_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
hc_error,
plugin::{PluginId, PluginName, PluginPublisher, PluginVersion},
string_newtype_parse_kdl_node,
util::kdl::{extract_data, ParseKdlNode},
util::kdl::{extract_data, ParseKdlNode, ToKdlNode},
};

use kdl::KdlNode;
Expand All @@ -23,6 +23,15 @@ pub enum ManifestLocation {
Local(PathBuf),
}

impl Display for ManifestLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ManifestLocation::Url(url) => write!(f, "{}", url.as_str()),
ManifestLocation::Local(path_buf) => write!(f, "{}", path_buf.to_string_lossy()),
}
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PolicyPlugin {
pub name: PolicyPluginName,
Expand Down
2 changes: 1 addition & 1 deletion hipcheck/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ fn load_policy_and_data(policy_path: Option<&Path>) -> Result<(PolicyFile, PathB

// Load the policy file.
let policy = PolicyFile::load_from(valid_policy_path)
.context("Failed to load policy. Plase make sure the policy file is in the proidved location and is formatted correctly.")?;
.context("Failed to load policy. Plase make sure the policy file is in the provided location and is formatted correctly.")?;

// Resolve the github token file.
let hc_github_token = resolve_token()?;
Expand Down
19 changes: 17 additions & 2 deletions hipcheck/src/util/kdl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ where
fn parse_node(node: &KdlNode) -> Option<Self>;
}

pub trait ToKdlNode {
/// convert self to a KdlNode
fn to_kdl_node(&self) -> KdlNode;
}

/// Returns the first successful node that can be parsed into T, if there is one
pub fn extract_data<T>(nodes: &[KdlNode]) -> Option<T>
where
Expand All @@ -33,8 +38,9 @@ where
/// code is quite repetitive for this simple task.
///
/// As a bonus, the following code is also generated:
/// - AsRef<String>
/// - new(value: String) -> Self
/// - `AsRef<String>`
/// - `new(value: String) -> Self`
/// - `ToKdlNode`
///
/// NOTE: This only works with newtype wrappers around String!
///
Expand Down Expand Up @@ -75,5 +81,14 @@ macro_rules! string_newtype_parse_kdl_node {
&self.0
}
}

impl ToKdlNode for $type {
#[allow(unused)]
fn to_kdl_node(&self) -> KdlNode {
let mut node = KdlNode::new(Self::kdl_key());
node.insert(0, self.0.clone());
node
}
}
};
}

0 comments on commit 56dd7a7

Please sign in to comment.