diff --git a/Cargo.toml b/Cargo.toml index f8a49522b..61604c545 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ - "cfn-guard" + "cfn-guard", + "cfn-guard-lambda" ] diff --git a/cfn-guard-lambda/Cargo.toml b/cfn-guard-lambda/Cargo.toml new file mode 100644 index 000000000..4a3263ea0 --- /dev/null +++ b/cfn-guard-lambda/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "cfn-guard-lambda" +version = "1.0.0" +authors = ["Omkar Hegde ", "aws-cloudformation-developers "] +description = "Lambda version of cfn-guard. Checks AWS CloudFormation templates for policy compliance using a simple, policy-as-code, declarative syntax" +license = "Apache-2.0" +edition = "2018" + +[dependencies] +lambda_runtime = "0.2.1" +serde = "1.0.92" +serde_derive = "1.0.92" +simple-error = "0.2.0" +simple_logger = "1.3.0" +log = "0.4.6" +cfn-guard = { version = "0.10.0", path = "../cfn-guard" } diff --git a/cfn-guard-lambda/src/main.rs b/cfn-guard-lambda/src/main.rs new file mode 100644 index 000000000..53f08e9e6 --- /dev/null +++ b/cfn-guard-lambda/src/main.rs @@ -0,0 +1,44 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::error::Error; + +use cfn_guard; +use lambda_runtime::{error::HandlerError, lambda, Context}; +use log::{self, info}; +use serde_derive::{Deserialize, Serialize}; +use simple_logger; + +#[derive(Deserialize, Debug)] +struct CustomEvent { + #[serde(rename = "data")] + data: String, + #[serde(rename = "rules")] + rules: String, +} + +#[derive(Serialize)] +struct CustomOutput { + message: String, +} + +fn main() -> Result<(), Box> { + simple_logger::init_with_level(log::Level::Info)?; + lambda!(call_cfn_guard); + + Ok(()) +} + +fn call_cfn_guard(e: CustomEvent, _c: Context) -> Result { + info!("Template is [{}]", &e.data); + info!("Rule Set is [{}]", &e.rules); + let (result) = match cfn_guard::run_checks(&e.data, &e.rules) + { + Ok(t) => t, + Err(e) => (e.to_string()), + }; + + Ok(CustomOutput { + message: result, + }) +} diff --git a/cfn-guard/Cargo.toml b/cfn-guard/Cargo.toml index 582aa2e58..972ce3990 100644 --- a/cfn-guard/Cargo.toml +++ b/cfn-guard/Cargo.toml @@ -5,6 +5,9 @@ authors = ["diwakar "] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "cfn_guard" +path = "src/lib.rs" [dependencies] nom = "5.1.2" diff --git a/cfn-guard/src/commands/helper.rs b/cfn-guard/src/commands/helper.rs index e69de29bb..c9be59a04 100644 --- a/cfn-guard/src/commands/helper.rs +++ b/cfn-guard/src/commands/helper.rs @@ -0,0 +1,40 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use crate::rules::errors::{Error, ErrorKind}; +use crate::rules::evaluate::RootScope; +use crate::rules::path_value::PathAwareValue; +use crate::commands::tracker::StackTracker; +use crate::commands::validate::ConsoleReporter; +use crate::rules::{Evaluate, Result}; +use std::convert::TryFrom; + +pub fn validate_and_return_json( + data: &str, + rules: &str, +) -> Result { + let mut input_data = match serde_json::from_str::(&data) { + Ok(value) => PathAwareValue::try_from(value), + Err(e) => return Err(Error::new(ErrorKind::ParseError(e.to_string()))), + }; + + let span = crate::rules::parser::Span::new_extra(&rules, "lambda"); + + match crate::rules::parser::rules_file(span) { + + Ok(rules) => { + match input_data { + Ok(root) => { + let root_context = RootScope::new(&rules, &root); + let stacker = StackTracker::new(&root_context); + let reporter = ConsoleReporter::new(stacker, true, true); + rules.evaluate(&root, &reporter)?; + let json_result = reporter.get_result_json(); + return Ok((json_result)); + } + Err(e) => return Err(e), + } + } + Err(e) => return Err(Error::new(ErrorKind::ParseError(e.to_string()))), + } +} diff --git a/cfn-guard/src/commands/mod.rs b/cfn-guard/src/commands/mod.rs index a88fc5898..595dbf01e 100644 --- a/cfn-guard/src/commands/mod.rs +++ b/cfn-guard/src/commands/mod.rs @@ -1,10 +1,10 @@ pub(crate) mod files; pub(crate) mod validate; pub(crate) mod test; +pub(crate) mod helper; pub(crate) mod parse_tree; mod tracker; -mod helper; // // Constants for arguments diff --git a/cfn-guard/src/commands/tracker.rs b/cfn-guard/src/commands/tracker.rs index d4132efde..9a7ce6f75 100644 --- a/cfn-guard/src/commands/tracker.rs +++ b/cfn-guard/src/commands/tracker.rs @@ -28,7 +28,7 @@ impl StatusContext { } } -pub(super) struct StackTracker<'r> { +pub(crate) struct StackTracker<'r> { root_context: &'r dyn EvaluationContext, stack: std::cell::RefCell>, } diff --git a/cfn-guard/src/commands/validate.rs b/cfn-guard/src/commands/validate.rs index ede618621..b79813670 100644 --- a/cfn-guard/src/commands/validate.rs +++ b/cfn-guard/src/commands/validate.rs @@ -96,8 +96,38 @@ impl Command for Validate { } } +pub fn validate_and_return_json( + data: &str, + rules: &str, +) -> Result { + let mut input_data = match serde_json::from_str::(&data) { + Ok(value) => PathAwareValue::try_from(value), + Err(e) => return Err(Error::new(ErrorKind::ParseError(e.to_string()))), + }; + + let span = crate::rules::parser::Span::new_extra(&rules, "lambda"); + + match crate::rules::parser::rules_file(span) { + + Ok(rules) => { + match input_data { + Ok(root) => { + let root_context = RootScope::new(&rules, &root); + let stacker = StackTracker::new(&root_context); + let reporter = ConsoleReporter::new(stacker, true, true); + rules.evaluate(&root, &reporter)?; + let json_result = reporter.get_result_json(); + return Ok((json_result)); + } + Err(e) => return Err(e), + } + } + Err(e) => return Err(Error::new(ErrorKind::ParseError(e.to_string()))), + } +} + #[derive(Debug)] -struct ConsoleReporter<'r> { +pub(crate) struct ConsoleReporter<'r> { root_context: StackTracker<'r>, verbose: bool, print_json: bool @@ -159,7 +189,7 @@ pub(super) fn print_context(cxt: &StatusContext, depth: usize) { } impl<'r, 'loc> ConsoleReporter<'r> { - fn new(root: StackTracker<'r>, verbose: bool, print_json: bool) -> Self { + pub(crate) fn new(root: StackTracker<'r>, verbose: bool, print_json: bool) -> Self { ConsoleReporter { root_context: root, verbose, @@ -167,6 +197,12 @@ impl<'r, 'loc> ConsoleReporter<'r> { } } + pub fn get_result_json(self) -> String { + let stack = self.root_context.stack(); + let top = stack.first().unwrap(); + return format!("{}", serde_json::to_string_pretty(&top.children).unwrap()); + } + fn report(self) { let stack = self.root_context.stack(); let top = stack.first().unwrap(); diff --git a/cfn-guard/src/lib.rs b/cfn-guard/src/lib.rs new file mode 100644 index 000000000..3d693ac78 --- /dev/null +++ b/cfn-guard/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +mod rules; +mod commands; +mod command; + +pub extern "C" fn run_checks( + data: &str, + rules: &str, +) -> crate::rules::Result { + return crate::commands::helper::validate_and_return_json(&data, &rules); +} diff --git a/cfn-guard/tests/functional.rs b/cfn-guard/tests/functional.rs new file mode 100644 index 000000000..d62fbc093 --- /dev/null +++ b/cfn-guard/tests/functional.rs @@ -0,0 +1,103 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use cfn_guard; + +mod tests { + use super::*; + #[test] + fn test_run_check() { + let mut data = String::from( + r#" + { + "Resources": { + "VPC" : { + "Type" : "AWS::ApiGateway::Method", + "Properties" : { + "AuthorizationType" : "10.0.0.0/24" + } + } + } + } + "#, + ); + let rule = "AWS::ApiGateway::Method { Properties.AuthorizationType == \"NONE\"}"; + let mut expected = String::from( + r#" + [ + { + "eval_type": "Rule", + "context": "default", + "from": null, + "to": null, + "status": "FAIL", + "children": [ + { + "eval_type": "Type", + "context": "AWS::ApiGateway::Method", + "from": null, + "to": null, + "status": "FAIL", + "children": [ + { + "eval_type": "Filter", + "context": "Path=/Resources/VPC,Type=MapElement", + "from": null, + "to": null, + "status": "PASS", + "children": [ + { + "eval_type": "Clause", + "context": "GuardAccessClause[ check = Type EQUALS String(\"AWS::ApiGateway::Method\"), loc = Location[file=#1@14] ]", + "from": null, + "to": null, + "status": "PASS", + "children": [] + } + ] + }, + { + "eval_type": "Type", + "context": "AWS::ApiGateway::Method#0(/Resources/VPC)", + "from": null, + "to": null, + "status": "FAIL", + "children": [ + { + "eval_type": "Clause", + "context": "GuardAccessClause[ check = Properties.AuthorizationType EQUALS String(\"NONE\"), loc = Location[file=lambda#1@27] ]", + "from": { + "String": [ + "/Resources/VPC/Properties/AuthorizationType", + "10.0.0.0/24" + ] + }, + "to": { + "String": [ + "lambda/1/27/Clause/", + "NONE" + ] + }, + "status": "FAIL", + "children": [] + } + ] + } + ] + } + ] + } + ] + "#, + ); + + // Remove white spaces from expected and calculated result for easy comparison. + expected.retain(|c| !c.is_whitespace()); + + let mut serialized = cfn_guard::run_checks(&data, &rule).unwrap(); + serialized.retain(|c| !c.is_whitespace()); + + assert_eq!(expected, serialized); + } + +}