diff --git a/hipcheck/src/plugin/kdl_parsing.rs b/hipcheck/src/kdl_helper.rs similarity index 94% rename from hipcheck/src/plugin/kdl_parsing.rs rename to hipcheck/src/kdl_helper.rs index 7c7efd72..095c0d34 100644 --- a/hipcheck/src/plugin/kdl_parsing.rs +++ b/hipcheck/src/kdl_helper.rs @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! General shared types and functions for KDL files + use kdl::KdlNode; // Helper trait to make it easier to parse KdlNodes into our own types @@ -44,6 +48,7 @@ where macro_rules! string_newtype_parse_kdl_node { ($type:ty, $identifier:expr) => { impl $type { + #[allow(dead_code)] pub fn new(value: String) -> Self { Self(value) } diff --git a/hipcheck/src/main.rs b/hipcheck/src/main.rs index 6dc6651f..3cd78135 100644 --- a/hipcheck/src/main.rs +++ b/hipcheck/src/main.rs @@ -13,9 +13,11 @@ mod data; mod engine; mod error; mod init; +mod kdl_helper; mod metric; #[allow(unused)] mod plugin; +mod policy; mod policy_exprs; mod report; mod session; diff --git a/hipcheck/src/plugin/download_manifest.rs b/hipcheck/src/plugin/download_manifest.rs index 1d684c38..1674fa60 100644 --- a/hipcheck/src/plugin/download_manifest.rs +++ b/hipcheck/src/plugin/download_manifest.rs @@ -1,9 +1,9 @@ -use super::{extract_data, PluginName, PluginPublisher, PluginVersion}; +use super::{PluginName, PluginPublisher, PluginVersion}; use crate::cache::plugin_cache::HcPluginCache; use crate::context::Context; +use crate::kdl_helper::{extract_data, ParseKdlNode}; use crate::plugin::retrieval::{download_plugin, extract_plugin}; use crate::plugin::supported_arch::SupportedArch; -use crate::plugin::ParseKdlNode; use crate::string_newtype_parse_kdl_node; use crate::util::http::agent::agent; use crate::{error::Error, hc_error}; diff --git a/hipcheck/src/plugin/mod.rs b/hipcheck/src/plugin/mod.rs index 8459f4ef..7d4058ef 100644 --- a/hipcheck/src/plugin/mod.rs +++ b/hipcheck/src/plugin/mod.rs @@ -1,11 +1,12 @@ mod download_manifest; -mod kdl_parsing; mod manager; mod plugin_manifest; mod retrieval; mod supported_arch; mod types; +use crate::context::Context; +use crate::kdl_helper::{extract_data, ParseKdlNode}; pub use crate::plugin::manager::*; pub use crate::plugin::types::*; use crate::policy_exprs::Expr; @@ -14,7 +15,6 @@ pub use download_manifest::{ ArchiveFormat, DownloadManifest, DownloadManifestEntry, HashAlgorithm, HashWithDigest, }; use futures::future::join_all; -pub use kdl_parsing::{extract_data, ParseKdlNode}; pub use plugin_manifest::{PluginManifest, PluginName, PluginPublisher, PluginVersion}; use serde_json::Value; use std::collections::HashMap; diff --git a/hipcheck/src/plugin/plugin_manifest.rs b/hipcheck/src/plugin/plugin_manifest.rs index 4c730603..bd89b557 100644 --- a/hipcheck/src/plugin/plugin_manifest.rs +++ b/hipcheck/src/plugin/plugin_manifest.rs @@ -1,6 +1,5 @@ -use super::extract_data; +use crate::kdl_helper::{extract_data, ParseKdlNode}; use crate::plugin::supported_arch::SupportedArch; -use crate::plugin::ParseKdlNode; use crate::string_newtype_parse_kdl_node; use crate::{error::Error, hc_error}; use core::panic; diff --git a/hipcheck/src/policy/mod.rs b/hipcheck/src/policy/mod.rs new file mode 100644 index 00000000..4f393489 --- /dev/null +++ b/hipcheck/src/policy/mod.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Data types and functions for parsing policy KDL files + +mod policy_file; +mod tests; + +use crate::kdl_helper::extract_data; +use crate::policy::policy_file::{PolicyAnalyze, PolicyPluginList}; +use crate::util::fs as file; +use crate::{error::Result, hc_error}; +use kdl::KdlDocument; +use std::path::Path; +use std::str::FromStr; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PolicyFile { + pub plugins: PolicyPluginList, + pub analyze: PolicyAnalyze, +} + +impl FromStr for PolicyFile { + type Err = crate::Error; + + fn from_str(s: &str) -> Result<Self> { + let document = + KdlDocument::from_str(s).map_err(|e| hc_error!("Error parsing policy file: {}", e))?; + let nodes = document.nodes(); + + let plugins: PolicyPluginList = + extract_data(nodes).ok_or_else(|| hc_error!("Could not parse 'plugins'"))?; + let analyze: PolicyAnalyze = + extract_data(nodes).ok_or_else(|| hc_error!("Could not parse 'analyze'"))?; + + Ok(Self { plugins, analyze }) + } +} + +impl PolicyFile { + /// Load policy from the given file. + pub fn load_from(policy_path: &Path) -> Result<PolicyFile> { + if policy_path.is_dir() { + return Err(hc_error!( + "Hipcheck policy path must be a file, not a directory." + )); + } + file::exists(policy_path)?; + let policy = PolicyFile::from_str(&file::read_string(policy_path)?)?; + + Ok(policy) + } +} diff --git a/hipcheck/src/policy/policy_file.rs b/hipcheck/src/policy/policy_file.rs new file mode 100644 index 00000000..59e3b4f0 --- /dev/null +++ b/hipcheck/src/policy/policy_file.rs @@ -0,0 +1,471 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Data types and functions for use in parsing policy KDL files + +use crate::error::Result; +use crate::hc_error; +use crate::kdl_helper::{extract_data, ParseKdlNode}; +use crate::string_newtype_parse_kdl_node; + +use kdl::KdlNode; +use std::collections::HashMap; +use std::fmt; +use std::fmt::Display; +use url::Url; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PolicyPlugin { + name: PolicyPluginName, + version: String, + manifest: Option<Url>, +} + +impl PolicyPlugin { + #[allow(dead_code)] + pub fn new(name: PolicyPluginName, version: String, manifest: Option<Url>) -> Self { + Self { + name, + version, + manifest, + } + } +} + +impl ParseKdlNode for PolicyPlugin { + fn kdl_key() -> &'static str { + "plugin" + } + + fn parse_node(node: &KdlNode) -> Option<Self> { + if node.name().to_string().as_str() != Self::kdl_key() { + return None; + } + + // per RFD #0004, the name is the first positional entry and has type String + // We split it into separate publisher and name fields here for use when downloading plugins downstream + let full_name = node.entries().first()?.value().as_string()?; + let name = match PolicyPluginName::new(full_name) { + Ok(name) => name, + Err(e) => { + log::error!("{}", e); + return None; + } + }; + let version = node.get("version")?.value().as_string()?.to_string(); + + // The manifest is technically optional, as there should be a default Hipcheck plugin artifactory sometime in the future + // But for now it is essentially mandatory, so a plugin without a manifest will return an error downstream + let manifest = match node.get("manifest") { + Some(entry) => { + let raw_url = entry.value().as_string()?; + match Url::parse(raw_url) { + Ok(url) => Some(url), + Err(_) => { + log::error!("Unable to parse provided manifest URL {} for plugin {} in the policy file", raw_url, name.to_string()); + return None; + } + } + } + None => None, + }; + + Some(Self { + name, + version, + manifest, + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PolicyPluginList(pub Vec<PolicyPlugin>); + +impl PolicyPluginList { + pub fn new() -> Self { + Self(Vec::new()) + } + + #[allow(dead_code)] + pub fn with_capacity(capacity: usize) -> Self { + Self(Vec::with_capacity(capacity)) + } + + pub fn push(&mut self, plugin: PolicyPlugin) { + self.0.push(plugin); + } + + #[allow(dead_code)] + pub fn pop(&mut self) -> Option<PolicyPlugin> { + self.0.pop() + } +} + +impl ParseKdlNode for PolicyPluginList { + fn kdl_key() -> &'static str { + "plugins" + } + + fn parse_node(node: &KdlNode) -> Option<Self> { + if node.name().to_string().as_str() != Self::kdl_key() { + return None; + } + + let mut plugins = Self::new(); + + for node in node.children()?.nodes() { + if let Some(dep) = PolicyPlugin::parse_node(node) { + plugins.push(dep); + } + } + + Some(plugins) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PolicyConfig(pub HashMap<String, String>); + +impl PolicyConfig { + pub fn new() -> Self { + Self(HashMap::new()) + } + + pub fn insert(&mut self, description: String, info: String) -> Result<()> { + match self.0.insert(description.clone(), info) { + Some(_duplicate_key) => Err(hc_error!( + "Duplicate configuration information specified for {}", + description + )), + None => Ok(()), + } + } + + #[allow(dead_code)] + pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> { + self.0.iter() + } +} + +impl ParseKdlNode for PolicyConfig { + fn kdl_key() -> &'static str { + "config" + } + + fn parse_node(node: &KdlNode) -> Option<Self> { + let mut config = PolicyConfig::new(); + for node in node.children()?.nodes() { + let description = node.name().to_string(); + if let Some(info) = node.entries().first() { + if config + .insert(description.clone(), info.value().as_string()?.to_string()) + .is_err() + { + log::error!( + "Duplicate configuration information detected for {}", + description + ); + return None; + } + } + } + Some(config) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PolicyAnalysis { + name: PolicyPluginName, + policy_expression: Option<String>, + weight: Option<u16>, + config: Option<PolicyConfig>, +} + +impl PolicyAnalysis { + #[allow(dead_code)] + pub fn new( + name: PolicyPluginName, + policy_expression: Option<String>, + weight: Option<u16>, + config: Option<PolicyConfig>, + ) -> Self { + Self { + name, + policy_expression, + weight, + config, + } + } +} + +impl ParseKdlNode for PolicyAnalysis { + fn kdl_key() -> &'static str { + "analysis" + } + + fn parse_node(node: &KdlNode) -> Option<Self> { + if node.name().to_string().as_str() != Self::kdl_key() { + return None; + } + + let full_name = node.entries().first()?.value().as_string()?; + let name = match PolicyPluginName::new(full_name) { + Ok(name) => name, + Err(e) => { + log::error!("{}", e); + return None; + } + }; + let policy_expression = match node.get("policy") { + Some(entry) => Some(entry.value().as_string()?.to_string()), + None => None, + }; + let weight = match node.get("weight") { + Some(entry) => Some(entry.value().as_i64()? as u16), + None => None, + }; + + let config = match node.children() { + Some(_) => PolicyConfig::parse_node(node), + None => None, + }; + + Some(Self { + name, + policy_expression, + weight, + config, + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PolicyCategory { + name: String, + children: Vec<PolicyCategoryChild>, +} + +impl PolicyCategory { + #[allow(dead_code)] + pub fn new(name: String) -> Self { + Self { + name, + children: Vec::new(), + } + } + + #[allow(dead_code)] + pub fn with_capacity(name: String, capacity: usize) -> Self { + Self { + name, + children: Vec::with_capacity(capacity), + } + } + + #[allow(dead_code)] + pub fn push(&mut self, child: PolicyCategoryChild) { + self.children.push(child); + } + + #[allow(dead_code)] + pub fn pop(&mut self) -> Option<PolicyCategoryChild> { + self.children.pop() + } +} + +impl ParseKdlNode for PolicyCategory { + fn kdl_key() -> &'static str { + "category" + } + + fn parse_node(node: &KdlNode) -> Option<Self> { + if node.name().to_string().as_str() != Self::kdl_key() { + return None; + } + + let name = node.entries().first()?.value().as_string()?.to_string(); + + let mut children = Vec::new(); + + // A category can contain both analyses and further subcategories + for node in node.children()?.nodes() { + if node.name().to_string().as_str() == "analysis" { + if let Some(analysis) = PolicyAnalysis::parse_node(node) { + children.push(PolicyCategoryChild::Analysis(analysis)); + } + } else if node.name().to_string().as_str() == "category" { + if let Some(category) = PolicyCategory::parse_node(node) { + children.push(PolicyCategoryChild::Category(category)); + } + } + } + + Some(Self { name, children }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PolicyCategoryChild { + Analysis(PolicyAnalysis), + Category(PolicyCategory), +} +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct InvestigatePolicy(pub String); +string_newtype_parse_kdl_node!(InvestigatePolicy, "investigate"); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct InvestigateIfFail(Vec<PolicyPluginName>); + +impl InvestigateIfFail { + #[allow(dead_code)] + pub fn new() -> Self { + Self(Vec::new()) + } + + #[allow(dead_code)] + pub fn with_capacity(capacity: usize) -> Self { + Self(Vec::with_capacity(capacity)) + } + + #[allow(dead_code)] + pub fn push(&mut self, plugin_name: &str) { + if let Ok(plugin) = PolicyPluginName::new(plugin_name) { + self.0.push(plugin); + } + } + + #[allow(dead_code)] + pub fn push_plugin(&mut self, plugin: PolicyPluginName) { + self.0.push(plugin); + } + + #[allow(dead_code)] + pub fn pop(&mut self) -> Option<PolicyPluginName> { + self.0.pop() + } +} + +impl ParseKdlNode for InvestigateIfFail { + fn kdl_key() -> &'static str { + "investigate-if-fail" + } + + fn parse_node(node: &KdlNode) -> Option<Self> { + if node.name().to_string().as_str() != Self::kdl_key() { + return None; + } + + let mut policies = Vec::new(); + + for node in node.entries() { + // Trim leading and trailing quotation marks from each policy in the list + let mut policy = node.value().to_string(); + policy.remove(0); + policy.pop(); + policies.push(PolicyPluginName::new(&policy).ok()?) + } + + Some(Self(policies)) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PolicyAnalyze { + investigate_policy: InvestigatePolicy, + if_fail: Option<InvestigateIfFail>, + categories: Vec<PolicyCategory>, +} + +impl PolicyAnalyze { + #[allow(dead_code)] + pub fn new(investigate_policy: InvestigatePolicy, if_fail: Option<InvestigateIfFail>) -> Self { + Self { + investigate_policy, + if_fail, + categories: Vec::new(), + } + } + + #[allow(dead_code)] + pub fn with_capacity( + investigate_policy: InvestigatePolicy, + if_fail: Option<InvestigateIfFail>, + capacity: usize, + ) -> Self { + Self { + investigate_policy, + if_fail, + categories: Vec::with_capacity(capacity), + } + } + + #[allow(dead_code)] + pub fn push(&mut self, category: PolicyCategory) { + self.categories.push(category); + } + + #[allow(dead_code)] + pub fn pop(&mut self) -> Option<PolicyCategory> { + self.categories.pop() + } +} + +impl ParseKdlNode for PolicyAnalyze { + fn kdl_key() -> &'static str { + "analyze" + } + + fn parse_node(node: &KdlNode) -> Option<Self> { + if node.name().to_string().as_str() != Self::kdl_key() { + return None; + } + + let nodes = node.children()?.nodes(); + + let investigate_policy: InvestigatePolicy = extract_data(nodes)?; + let if_fail: Option<InvestigateIfFail> = extract_data(nodes); + + let mut categories = Vec::new(); + + for node in nodes { + if node.name().to_string().as_str() == "category" { + if let Some(category) = PolicyCategory::parse_node(node) { + categories.push(category); + } + } + } + + Some(Self { + investigate_policy, + if_fail, + categories, + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PolicyPluginName { + publisher: String, + name: String, +} + +impl PolicyPluginName { + pub fn new(full_name: &str) -> Result<Self> { + let parsed_name: Vec<&str> = full_name.split('/').collect(); + if parsed_name.len() > 1 { + let publisher = parsed_name[0].to_string(); + let name = parsed_name[1].to_string(); + Ok(Self { publisher, name }) + } else { + Err(hc_error!( + "Provided policy {} is not in the format {{publisher}}/{{name}}", + full_name + )) + } + } +} + +impl Display for PolicyPluginName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}/{}", self.publisher, self.name) + } +} diff --git a/hipcheck/src/policy/tests.rs b/hipcheck/src/policy/tests.rs new file mode 100644 index 00000000..6a8f1e30 --- /dev/null +++ b/hipcheck/src/policy/tests.rs @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Tests of policy file parsing functions +#[cfg(test)] +mod test { + use crate::kdl_helper::ParseKdlNode; + use crate::policy::policy_file::*; + use crate::policy::PolicyFile; + + use kdl::KdlNode; + use std::str::FromStr; + use url::Url; + + #[test] + fn test_parsing_plugin() { + let data = r#"plugin "mitre/activity" version="0.1.0" manifest="https://github.com/mitre/hipcheck/blob/main/plugin/dist/mitre-activity.kdl""#; + let node = KdlNode::from_str(data).unwrap(); + + let expected = PolicyPlugin::new( + PolicyPluginName::new("mitre/activity").unwrap(), + "0.1.0".to_string(), + Some( + Url::parse( + "https://github.com/mitre/hipcheck/blob/main/plugin/dist/mitre-activity.kdl", + ) + .unwrap(), + ), + ); + + assert_eq!(expected, PolicyPlugin::parse_node(&node).unwrap()) + } + + #[test] + fn test_parsing_plugin_list() { + let data = r#"plugins { + plugin "mitre/activity" version="0.1.0" manifest="https://github.com/mitre/hipcheck/blob/main/plugin/dist/mitre-activity.kdl" + plugin "mitre/binary" version="0.1.1" manifest="https://github.com/mitre/hipcheck/blob/main/plugin/dist/mitre-binary.kdl" + }"#; + let node = KdlNode::from_str(data).unwrap(); + + let mut expected = PolicyPluginList::new(); + expected.push(PolicyPlugin::new( + PolicyPluginName::new("mitre/activity").unwrap(), + "0.1.0".to_string(), + Some( + Url::parse( + "https://github.com/mitre/hipcheck/blob/main/plugin/dist/mitre-activity.kdl", + ) + .unwrap(), + ), + )); + expected.push(PolicyPlugin::new( + PolicyPluginName::new("mitre/binary").unwrap(), + "0.1.1".to_string(), + Some( + Url::parse( + "https://github.com/mitre/hipcheck/blob/main/plugin/dist/mitre-binary.kdl", + ) + .unwrap(), + ), + )); + + assert_eq!(expected, PolicyPluginList::parse_node(&node).unwrap()) + } + + #[test] + fn test_parsing_investigate_policy() { + let data = r#"investigate policy="(gt 0.5 $)""#; + let node = KdlNode::from_str(data).unwrap(); + + let expected = InvestigatePolicy("(gt 0.5 $)".to_string()); + + assert_eq!(expected, InvestigatePolicy::parse_node(&node).unwrap()) + } + + #[test] + fn test_parsing_investigate_if_fail() { + let data = r#"investigate-if-fail "mitre/typo" "mitre/binary""#; + let node = KdlNode::from_str(data).unwrap(); + + let mut expected = InvestigateIfFail::new(); + expected.push("mitre/typo"); + expected.push("mitre/binary"); + + assert_eq!(expected, InvestigateIfFail::parse_node(&node).unwrap()) + } + + #[test] + fn test_parsing_analysis_weight() { + let data = r#"analysis "mitre/typo" policy="(eq 0 (count $))" weight=3"#; + let node = KdlNode::from_str(data).unwrap(); + + let mut config = PolicyConfig::new(); + config + .insert("typo-file".to_string(), "./config/typo.kdl".to_string()) + .unwrap(); + + let expected = PolicyAnalysis::new( + PolicyPluginName::new("mitre/typo").unwrap(), + Some("(eq 0 (count $))".to_string()), + Some(3), + None, + ); + + assert_eq!(expected, PolicyAnalysis::parse_node(&node).unwrap()) + } + + #[test] + fn test_parsing_analysis_config() { + let data = r#"analysis "mitre/typo" policy="(eq 0 (count $))" { + typo-file "./config/typo.kdl" + }"#; + let node = KdlNode::from_str(data).unwrap(); + + let mut config = PolicyConfig::new(); + config + .insert("typo-file".to_string(), "./config/typo.kdl".to_string()) + .unwrap(); + + let expected = PolicyAnalysis::new( + PolicyPluginName::new("mitre/typo").unwrap(), + Some("(eq 0 (count $))".to_string()), + None, + Some(config), + ); + + assert_eq!(expected, PolicyAnalysis::parse_node(&node).unwrap()) + } + + #[test] + fn test_parsing_analysis_multiple_configs() { + let data = r#"analysis "mitre/typo" policy="(eq 0 (count $))" weight=3 { + typo-file "./config/typo.kdl" + typo-file-2 "./config/typo2.kdl" + }"#; + let node = KdlNode::from_str(data).unwrap(); + + let mut config = PolicyConfig::new(); + config + .insert("typo-file".to_string(), "./config/typo.kdl".to_string()) + .unwrap(); + config + .insert("typo-file-2".to_string(), "./config/typo2.kdl".to_string()) + .unwrap(); + + let expected = PolicyAnalysis::new( + PolicyPluginName::new("mitre/typo").unwrap(), + Some("(eq 0 (count $))".to_string()), + Some(3), + Some(config), + ); + + assert_eq!(expected, PolicyAnalysis::parse_node(&node).unwrap()) + } + + #[test] + fn test_parse_analyze() { + let data = r#"analyze { + investigate policy="(gt 0.5 $)" + investigate-if-fail "mitre/typo" "mitre/binary" + + category "practices" { + analysis "mitre/activity" policy="(lte 52 $.weeks)" weight=3 + analysis "mitre/binary" policy="(eq 0 (count $))" + } + + category "attacks" { + analysis "mitre/typo" policy="(eq 0 (count $))" { + typo-file "./config/typo.kdl" + } + + category "commit" { + analysis "mitre/affiliation" policy="(eq 0 (count $))" { + orgs-file "./config/orgs.kdl" + } + + analysis "mitre/entropy" policy="(eq 0 (count (filter (gt 8.0) $)))" + analysis "mitre/churn" policy="(eq 0 (count (filter (gt 8.0) $)))" + } + } + }"#; + let node = KdlNode::from_str(data).unwrap(); + + let investigate_policy = InvestigatePolicy("(gt 0.5 $)".to_string()); + + let mut if_fail = InvestigateIfFail::new(); + if_fail.push("mitre/typo"); + if_fail.push("mitre/binary"); + + let mut practices = PolicyCategory::new("practices".to_string()); + practices.push(PolicyCategoryChild::Analysis(PolicyAnalysis::new( + PolicyPluginName::new("mitre/activity").unwrap(), + Some("(lte 52 $.weeks)".to_string()), + Some(3), + None, + ))); + practices.push(PolicyCategoryChild::Analysis(PolicyAnalysis::new( + PolicyPluginName::new("mitre/binary").unwrap(), + Some("(eq 0 (count $))".to_string()), + None, + None, + ))); + + let mut affiliation_config = PolicyConfig::new(); + affiliation_config + .insert("orgs-file".to_string(), "./config/orgs.kdl".to_string()) + .unwrap(); + + let mut typo_config = PolicyConfig::new(); + typo_config + .insert("typo-file".to_string(), "./config/typo.kdl".to_string()) + .unwrap(); + + let mut commit = PolicyCategory::new("commit".to_string()); + commit.push(PolicyCategoryChild::Analysis(PolicyAnalysis::new( + PolicyPluginName::new("mitre/affiliation").unwrap(), + Some("(eq 0 (count $))".to_string()), + None, + Some(affiliation_config), + ))); + commit.push(PolicyCategoryChild::Analysis(PolicyAnalysis::new( + PolicyPluginName::new("mitre/entropy").unwrap(), + Some("(eq 0 (count (filter (gt 8.0) $)))".to_string()), + None, + None, + ))); + commit.push(PolicyCategoryChild::Analysis(PolicyAnalysis::new( + PolicyPluginName::new("mitre/churn").unwrap(), + Some("(eq 0 (count (filter (gt 8.0) $)))".to_string()), + None, + None, + ))); + + let mut attacks = PolicyCategory::new("attacks".to_string()); + attacks.push(PolicyCategoryChild::Analysis(PolicyAnalysis::new( + PolicyPluginName::new("mitre/typo").unwrap(), + Some("(eq 0 (count $))".to_string()), + None, + Some(typo_config), + ))); + attacks.push(PolicyCategoryChild::Category(commit)); + + let mut expected = PolicyAnalyze::new(investigate_policy, Some(if_fail)); + expected.push(practices); + expected.push(attacks); + + assert_eq!(expected, PolicyAnalyze::parse_node(&node).unwrap()) + } + + #[test] + fn test_parse_policy_file() { + let data = r#"plugins { + plugin "mitre/activity" version="0.1.0" manifest="https://github.com/mitre/hipcheck/blob/main/plugin/dist/mitre-activity.kdl" + plugin "mitre/binary" version="0.1.1" manifest="https://github.com/mitre/hipcheck/blob/main/plugin/dist/mitre-binary.kdl" + } + + analyze { + investigate policy="(gt 0.5 $)" + investigate-if-fail "mitre/binary" + + category "practices" { + analysis "mitre/activity" policy="(lte 52 $.weeks)" weight=3 + analysis "mitre/binary" policy="(eq 0 (count $))" + } + }"#; + + let mut plugins = PolicyPluginList::new(); + plugins.push(PolicyPlugin::new( + PolicyPluginName::new("mitre/activity").unwrap(), + "0.1.0".to_string(), + Some( + Url::parse( + "https://github.com/mitre/hipcheck/blob/main/plugin/dist/mitre-activity.kdl", + ) + .unwrap(), + ), + )); + plugins.push(PolicyPlugin::new( + PolicyPluginName::new("mitre/binary").unwrap(), + "0.1.1".to_string(), + Some( + Url::parse( + "https://github.com/mitre/hipcheck/blob/main/plugin/dist/mitre-binary.kdl", + ) + .unwrap(), + ), + )); + + let investigate_policy = InvestigatePolicy("(gt 0.5 $)".to_string()); + + let mut if_fail = InvestigateIfFail::new(); + if_fail.push("mitre/binary"); + + let mut practices = PolicyCategory::new("practices".to_string()); + practices.push(PolicyCategoryChild::Analysis(PolicyAnalysis::new( + PolicyPluginName::new("mitre/activity").unwrap(), + Some("(lte 52 $.weeks)".to_string()), + Some(3), + None, + ))); + practices.push(PolicyCategoryChild::Analysis(PolicyAnalysis::new( + PolicyPluginName::new("mitre/binary").unwrap(), + Some("(eq 0 (count $))".to_string()), + None, + None, + ))); + + let mut analyze = PolicyAnalyze::new(investigate_policy, Some(if_fail)); + analyze.push(practices); + + let expected = PolicyFile { plugins, analyze }; + + assert_eq!(expected, PolicyFile::from_str(data).unwrap()) + } +} diff --git a/hipcheck/src/policy_exprs/mod.rs b/hipcheck/src/policy_exprs/mod.rs index 6a6dbea4..d6f30a7a 100644 --- a/hipcheck/src/policy_exprs/mod.rs +++ b/hipcheck/src/policy_exprs/mod.rs @@ -3,7 +3,7 @@ mod bridge; mod env; mod error; -mod expr; +pub mod expr; mod token; pub(crate) use crate::policy_exprs::bridge::Tokens; diff --git a/hipcheck/src/session/mod.rs b/hipcheck/src/session/mod.rs index f8916ffa..b8ff3069 100644 --- a/hipcheck/src/session/mod.rs +++ b/hipcheck/src/session/mod.rs @@ -35,6 +35,7 @@ use crate::hc_error; use crate::metric::binary_detector::BinaryFileStorage; use crate::metric::linguist::LinguistStorage; use crate::metric::MetricProviderStorage; +use crate::policy::PolicyFile; use crate::report::Format; use crate::report::ReportParams; use crate::report::ReportParamsStorage; @@ -159,14 +160,27 @@ impl Session { * Loading configuration. *-----------------------------------------------------------------*/ - // Check if a currently unsuporrted policy file was provided - // TODO: Remove this error once policy files are supported + // Check if a policy file was provided, otherwise use a config folder the old way + // Currently this does nothing, as the PolicyFile is not actively parsed if policy_path.is_some() { - return Err(hc_error!( - "Policy files are not supported by Hipcheck at this time." - )); - } + let (_policy, _policy_dir, _data_dir, _hc_github_token) = + match load_policy_and_data(policy_path.as_deref(), data_path.as_deref()) { + Ok(results) => results, + Err(err) => return Err(err), + }; + + // Needed if we use Salsa for this + // session.set_policy(Rc::new(policy)); + // session.set_policy_dir(Rc::new(policy_dir)); + // Set data folder location for module analysis + // session.set_data_dir(Arc::new(data_dir)); + + // Set github token in salsa + // session.set_github_api_token(Some(Rc::new(hc_github_token))); + } + // Once we no longer need config information, put this in an else block until config has been deprecated + // Until then we need this for Hipcheck to still run let (config, config_dir, data_dir, hc_github_token) = match load_config_and_data(config_path.as_deref(), data_path.as_deref()) { Ok(results) => results, @@ -273,6 +287,46 @@ fn load_config_and_data( )) } +fn load_policy_and_data( + policy_path: Option<&Path>, + data_path: Option<&Path>, +) -> Result<(PolicyFile, PathBuf, PathBuf, String)> { + // Start the phase. + let phase = SpinnerPhase::start("loading policy and data files"); + // Increment the phase into the "running" stage. + phase.inc(); + // Set the spinner phase to tick constantly, 10 times a second. + phase.enable_steady_tick(Duration::from_millis(100)); + + // Resolve the path to the policy file. + let valid_policy_path = policy_path.ok_or_else(|| { + hc_error!( + "Failed to load policy. Please make sure the path set by the --policy flag exists." + ) + })?; + + // 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 proived location and is formatted correctly.")?; + + // Get the directory the data file is in. + let data_dir = data_path + .ok_or_else(|| hc_error!("Failed to load data files. Please make sure the path set by the hc_data env variable exists."))? + .to_owned(); + + // Resolve the github token file. + let hc_github_token = resolve_token()?; + + phase.finish_successful(); + + Ok(( + policy, + valid_policy_path.to_path_buf(), + data_dir, + hc_github_token, + )) +} + fn load_target(seed: &TargetSeed, home: &Path) -> Result<Target> { // Resolve the source specifier into an actual source. let phase_desc = match seed.kind {