diff --git a/README.md b/README.md index 00b90a9..c4ff7ab 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ Arguments: -f, --function A specific function to scan. Must be an entrypoint specified in `manifest.yml` -h, --help Print help information -V, --version Print version information + --check-permissions Runs the permission checker + --graphql-schema-path Uses the graphql schema in location; othwerwise selects ~/.config dir ``` ## Installation @@ -64,6 +66,12 @@ until then you can test `fsrt` by manually invoking: fsrt ./test-apps/jira-damn-vulnerable-forge-app ``` +Testing with a GraphQl Schema: + +```sh +cargo test --features graphql_schema +``` + ## Contributions Contributions to FSRT are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details. diff --git a/crates/fsrt/Cargo.toml b/crates/fsrt/Cargo.toml index b21fd4b..411c898 100644 --- a/crates/fsrt/Cargo.toml +++ b/crates/fsrt/Cargo.toml @@ -8,6 +8,9 @@ license.workspace = true [lints] workspace = true +[features] +graphql_schema = [] + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/crates/fsrt/src/main.rs b/crates/fsrt/src/main.rs index eb164de..fa530f1 100644 --- a/crates/fsrt/src/main.rs +++ b/crates/fsrt/src/main.rs @@ -20,7 +20,7 @@ use std::{ use graphql_parser::{ query::{Mutation, Query, SelectionSet}, - schema::ObjectType, + schema::{EnumType, EnumValue, ObjectType}, }; use graphql_parser::{ @@ -618,6 +618,40 @@ pub(crate) fn scan_directory<'a>( &mut path.to_owned() }; + let mut scope_path = path.clone(); + + scope_path.push("schema/shared/agg-shared-scopes.nadel"); + + let scope_map = fs::read_to_string(&scope_path).unwrap_or_default(); + + let ast = parse_schema::<&str>(&scope_map).unwrap_or_default(); + + let mut scope_name_to_oauth = HashMap::new(); + + ast.definitions.iter().for_each(|val| { + if let graphql_parser::schema::Definition::TypeDefinition(TypeDefinition::Enum( + EnumType { values, .. }, + )) = val + { + values.iter().for_each( + |EnumValue { + directives, name, .. + }| { + if let Some(directive) = directives.first() { + if let graphql_parser::schema::Value::String(oauth_scope) = &directive + .arguments + .first() + .expect("Should only be one directive") + .1 + { + scope_name_to_oauth.insert(name, oauth_scope); + } + } + }, + ) + } + }); + path.push("schema/*/*.nadel"); let joined_schema = glob(path.to_str().unwrap_or_default()) @@ -653,10 +687,15 @@ pub(crate) fn scan_directory<'a>( used_graphql_perms.extend_from_slice(&graphql_perms_defid); used_graphql_perms.extend_from_slice(&graphql_perms_varid); + let oauth_scopes: Vec<&&String> = used_graphql_perms + .iter() + .map(|val| scope_name_to_oauth.get(&val.as_str()).unwrap()) + .collect(); + let final_perms: Vec<&String> = perm_interp .permissions .iter() - .filter(|f| !used_graphql_perms.contains(&**f)) + .filter(|f| !oauth_scopes.contains(&f)) .collect(); if run_permission_checker && !final_perms.is_empty() { diff --git a/crates/fsrt/src/test.rs b/crates/fsrt/src/test.rs index 74f462e..a628e73 100644 --- a/crates/fsrt/src/test.rs +++ b/crates/fsrt/src/test.rs @@ -17,6 +17,7 @@ trait ReportExt { fn contains_secret_vuln(&self, expected_len: usize) -> bool; + #[cfg(feature = "graphql_schema")] fn contains_perm_vuln(&self, expected_len: usize) -> bool; fn contains_vulns(&self, expected_len: i32) -> bool; @@ -48,6 +49,7 @@ impl ReportExt for Report { == expected_len } + #[cfg(feature = "graphql_schema")] #[inline] fn contains_perm_vuln(&self, expected_len: usize) -> bool { self.into_vulns() @@ -336,7 +338,6 @@ fn secret_vuln_object() { ); let scan_result = scan_directory_test(test_forge_project); - println!("scan_result {scan_result:?}"); assert!(scan_result.contains_secret_vuln(1)); assert!(scan_result.contains_vulns(1)) } @@ -635,13 +636,12 @@ fn basic_authz_vuln() { ); let scan_result = scan_directory_test(test_forge_project); - println!("vuln, {:#?}", scan_result); assert!(scan_result.contains_authz_vuln(1)); assert!(scan_result.contains_vulns(1)); } +#[cfg(feature = "graphql_schema")] #[test] -#[ignore] fn excess_scope() { let mut test_forge_project = MockForgeProject::files_from_string( "// src/index.tsx @@ -663,13 +663,13 @@ fn excess_scope() { .push("read:component:compass".into()); let scan_result = scan_directory_test(test_forge_project); - println!("scan_result {:#?}", scan_result); assert!(scan_result.contains_perm_vuln(1)); assert!(scan_result.contains_vulns(1)) } +#[cfg(feature = "graphql_schema")] #[test] -fn correct_scopes() { +fn graphql_correct_scopes() { let mut test_forge_project = MockForgeProject::files_from_string( "// src/index.tsx import ForgeUI, { render, Macro } from '@forge/ui'; @@ -706,16 +706,15 @@ fn correct_scopes() { .test_manifest .permissions .scopes - .push("read:component:compass".into()); + .push("compass:atlassian-external".into()); let scan_result = scan_directory_test(test_forge_project); - println!("scan_result {:#?}", scan_result); assert!(scan_result.contains_vulns(0)) } +#[cfg(feature = "graphql_schema")] #[test] -#[ignore] -fn excess_scope_with_fragments() { +fn graphql_excess_scope_with_fragments() { let mut test_forge_project = MockForgeProject::files_from_string( "// src/index.tsx import ForgeUI, { render, Macro } from '@forge/ui'; @@ -740,13 +739,13 @@ fn excess_scope_with_fragments() { .push("read:component:compass".into()); let scan_result = scan_directory_test(test_forge_project); - println!("scan_result {:#?}", scan_result); assert!(scan_result.contains_perm_vuln(1)); assert!(scan_result.contains_vulns(1)) } +#[cfg(feature = "graphql_schema")] #[test] -fn correct_scopes_with_fragment() { +fn graphql_correct_scopes_with_fragment() { let mut test_forge_project = MockForgeProject::files_from_string( "// src/index.tsx import ForgeUI, { render, Macro } from '@forge/ui'; @@ -769,10 +768,9 @@ fn correct_scopes_with_fragment() { .test_manifest .permissions .scopes - .push("read:component:compass".into()); + .push("compass:atlassian-external".into()); let scan_result = scan_directory_test(test_forge_project); - println!("scan_result {:#?}", scan_result); assert!(scan_result.contains_vulns(0)) } @@ -887,6 +885,5 @@ fn authz_function_called_in_object_bitbucket() { ); let scan_result = scan_directory_test(test_forge_project); - println!("scan_result {:#?}", scan_result); assert!(scan_result.contains_vulns(1)) }