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 {