Skip to content

Commit 9ee164f

Browse files
author
gersbach
committed
EAS-2582 : Map Scopes to OAuth
1 parent 7fa85d2 commit 9ee164f

File tree

4 files changed

+78
-25
lines changed

4 files changed

+78
-25
lines changed

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Arguments:
2121
-f, --function <FUNCTION> A specific function to scan. Must be an entrypoint specified in `manifest.yml`
2222
-h, --help Print help information
2323
-V, --version Print version information
24+
--check-permissions Runs the permission checker
25+
--graphql-schema-path <LOCATION> Uses the graphql schema in location; othwerwise selects ~/.config dir
2426
```
2527

2628
## Installation
@@ -64,6 +66,12 @@ until then you can test `fsrt` by manually invoking:
6466
fsrt ./test-apps/jira-damn-vulnerable-forge-app
6567
```
6668

69+
Testing with a GraphQl Schema:
70+
71+
```sh
72+
cargo test --features graphql_schema
73+
```
74+
6775
## Contributions
6876

6977
Contributions to FSRT are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.

crates/fsrt/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ license.workspace = true
88
[lints]
99
workspace = true
1010

11+
[features]
12+
graphql_schema = []
13+
1114
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1215

1316
[dependencies]

crates/fsrt/src/main.rs

+56-11
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use std::{
2020

2121
use graphql_parser::{
2222
query::{Mutation, Query, SelectionSet},
23-
schema::ObjectType,
23+
schema::{EnumType, EnumValue, ObjectType},
2424
};
2525

2626
use graphql_parser::{
@@ -118,7 +118,7 @@ impl fmt::Display for Error {
118118
}
119119

120120
struct PermissionsAndNextSelection<'a, 'b> {
121-
permission_vec: Vec<String>,
121+
permission_vec: Vec<&'a str>,
122122
next_selection: NextSelection<'a, 'b>,
123123
}
124124

@@ -130,7 +130,7 @@ struct NextSelection<'a, 'b> {
130130
fn parse_grapqhql_schema<'a: 'b, 'b>(
131131
schema_doc: &'a [graphql_parser::schema::Definition<'a, &'a str>],
132132
query_doc: &'b [graphql_parser::query::Definition<'b, &'b str>],
133-
) -> Vec<String> {
133+
) -> Vec<&'a str> {
134134
let mut permission_list = vec![];
135135

136136
// dequeue of (parsed_query_selection: SelectionSet, schema_type_field: Field)
@@ -272,7 +272,7 @@ fn get_type_or_typex_with_name<'a, 'b>(
272272
.flatten()
273273
}
274274

275-
fn get_field_directives<'a>(field: &'a graphql_parser::schema::Field<'_, &'a str>) -> Vec<String> {
275+
fn get_field_directives<'a>(field: &'a graphql_parser::schema::Field<'_, &'a str>) -> Vec<&'a str> {
276276
let mut perm_vec = vec![];
277277
field.directives.iter().for_each(|directive| {
278278
if directive.name == "scopes" {
@@ -281,7 +281,7 @@ fn get_field_directives<'a>(field: &'a graphql_parser::schema::Field<'_, &'a str
281281
if let query::Value::List(val) = &arg.1 {
282282
val.iter().for_each(|val| {
283283
if let query::Value::Enum(en) = val {
284-
perm_vec.push(en.to_string());
284+
perm_vec.push(*en);
285285
}
286286
});
287287
}
@@ -295,7 +295,7 @@ fn get_field_directives<'a>(field: &'a graphql_parser::schema::Field<'_, &'a str
295295
fn check_graphql_and_perms<'a>(
296296
val: &'a Value,
297297
path: &'a graphql_parser::schema::Document<'a, &'a str>,
298-
) -> Vec<String> {
298+
) -> Vec<&'a str> {
299299
let mut operations = vec![];
300300

301301
match val {
@@ -618,6 +618,40 @@ pub(crate) fn scan_directory<'a>(
618618
&mut path.to_owned()
619619
};
620620

621+
let mut scope_path = path.clone();
622+
623+
scope_path.push("schema/shared/agg-shared-scopes.nadel");
624+
625+
let scope_map = fs::read_to_string(&scope_path).unwrap_or_default();
626+
627+
let ast = parse_schema::<&str>(&scope_map).unwrap_or_default();
628+
629+
let mut scope_name_to_oauth = HashMap::new();
630+
631+
ast.definitions.iter().for_each(|val| {
632+
if let graphql_parser::schema::Definition::TypeDefinition(TypeDefinition::Enum(
633+
EnumType { values, .. },
634+
)) = val
635+
{
636+
values.iter().for_each(
637+
|EnumValue {
638+
directives, name, ..
639+
}| {
640+
if let Some(directive) = directives.first() {
641+
if let graphql_parser::schema::Value::String(oauth_scope) = &directive
642+
.arguments
643+
.first()
644+
.expect("Should only be one directive")
645+
.1
646+
{
647+
scope_name_to_oauth.insert(name, oauth_scope);
648+
}
649+
}
650+
},
651+
)
652+
}
653+
});
654+
621655
path.push("schema/*/*.nadel");
622656

623657
let joined_schema = glob(path.to_str().unwrap_or_default())
@@ -629,21 +663,21 @@ pub(crate) fn scan_directory<'a>(
629663
let ast = parse_schema::<&str>(&joined_schema);
630664

631665
if let std::result::Result::Ok(doc) = ast {
632-
let mut used_graphql_perms: Vec<String> = definition_analysis_interp
666+
let mut used_graphql_perms: Vec<&str> = definition_analysis_interp
633667
.value_manager
634668
.varid_to_value_with_proj
635669
.values()
636670
.flat_map(|val| check_graphql_and_perms(val, &doc))
637671
.collect();
638672

639-
let graphql_perms_varid: Vec<String> = definition_analysis_interp
673+
let graphql_perms_varid: Vec<&str> = definition_analysis_interp
640674
.value_manager
641675
.varid_to_value
642676
.values()
643677
.flat_map(|val| check_graphql_and_perms(val, &doc))
644678
.collect();
645679

646-
let graphql_perms_defid: Vec<String> = definition_analysis_interp
680+
let graphql_perms_defid: Vec<&str> = definition_analysis_interp
647681
.value_manager
648682
.defid_to_value
649683
.values()
@@ -653,10 +687,21 @@ pub(crate) fn scan_directory<'a>(
653687
used_graphql_perms.extend_from_slice(&graphql_perms_defid);
654688
used_graphql_perms.extend_from_slice(&graphql_perms_varid);
655689

656-
let final_perms: Vec<&String> = perm_interp
690+
let oauth_scopes: HashSet<&&String> = used_graphql_perms
691+
.iter()
692+
.filter_map(|val| {
693+
if !scope_name_to_oauth.contains_key(&val) {
694+
warn!("Scope is not contained in the scope definitions")
695+
}
696+
697+
scope_name_to_oauth.get(&val)
698+
})
699+
.collect();
700+
701+
let final_perms: HashSet<&String> = perm_interp
657702
.permissions
658703
.iter()
659-
.filter(|f| !used_graphql_perms.contains(&**f))
704+
.filter(|f| !oauth_scopes.contains(&f))
660705
.collect();
661706

662707
if run_permission_checker && !final_perms.is_empty() {

crates/fsrt/src/test.rs

+11-14
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ trait ReportExt {
1717

1818
fn contains_secret_vuln(&self, expected_len: usize) -> bool;
1919

20+
#[cfg(feature = "graphql_schema")]
2021
fn contains_perm_vuln(&self, expected_len: usize) -> bool;
2122

2223
fn contains_vulns(&self, expected_len: i32) -> bool;
@@ -48,6 +49,7 @@ impl ReportExt for Report {
4849
== expected_len
4950
}
5051

52+
#[cfg(feature = "graphql_schema")]
5153
#[inline]
5254
fn contains_perm_vuln(&self, expected_len: usize) -> bool {
5355
self.into_vulns()
@@ -336,7 +338,6 @@ fn secret_vuln_object() {
336338
);
337339

338340
let scan_result = scan_directory_test(test_forge_project);
339-
println!("scan_result {scan_result:?}");
340341
assert!(scan_result.contains_secret_vuln(1));
341342
assert!(scan_result.contains_vulns(1))
342343
}
@@ -635,13 +636,12 @@ fn basic_authz_vuln() {
635636
);
636637

637638
let scan_result = scan_directory_test(test_forge_project);
638-
println!("vuln, {:#?}", scan_result);
639639
assert!(scan_result.contains_authz_vuln(1));
640640
assert!(scan_result.contains_vulns(1));
641641
}
642642

643+
#[cfg(feature = "graphql_schema")]
643644
#[test]
644-
#[ignore]
645645
fn excess_scope() {
646646
let mut test_forge_project = MockForgeProject::files_from_string(
647647
"// src/index.tsx
@@ -663,13 +663,13 @@ fn excess_scope() {
663663
.push("read:component:compass".into());
664664

665665
let scan_result = scan_directory_test(test_forge_project);
666-
println!("scan_result {:#?}", scan_result);
667666
assert!(scan_result.contains_perm_vuln(1));
668667
assert!(scan_result.contains_vulns(1))
669668
}
670669

670+
#[cfg(feature = "graphql_schema")]
671671
#[test]
672-
fn correct_scopes() {
672+
fn graphql_correct_scopes() {
673673
let mut test_forge_project = MockForgeProject::files_from_string(
674674
"// src/index.tsx
675675
import ForgeUI, { render, Macro } from '@forge/ui';
@@ -706,16 +706,15 @@ fn correct_scopes() {
706706
.test_manifest
707707
.permissions
708708
.scopes
709-
.push("read:component:compass".into());
709+
.push("compass:atlassian-external".into());
710710

711711
let scan_result = scan_directory_test(test_forge_project);
712-
println!("scan_result {:#?}", scan_result);
713712
assert!(scan_result.contains_vulns(0))
714713
}
715714

715+
#[cfg(feature = "graphql_schema")]
716716
#[test]
717-
#[ignore]
718-
fn excess_scope_with_fragments() {
717+
fn graphql_excess_scope_with_fragments() {
719718
let mut test_forge_project = MockForgeProject::files_from_string(
720719
"// src/index.tsx
721720
import ForgeUI, { render, Macro } from '@forge/ui';
@@ -740,13 +739,13 @@ fn excess_scope_with_fragments() {
740739
.push("read:component:compass".into());
741740

742741
let scan_result = scan_directory_test(test_forge_project);
743-
println!("scan_result {:#?}", scan_result);
744742
assert!(scan_result.contains_perm_vuln(1));
745743
assert!(scan_result.contains_vulns(1))
746744
}
747745

746+
#[cfg(feature = "graphql_schema")]
748747
#[test]
749-
fn correct_scopes_with_fragment() {
748+
fn graphql_correct_scopes_with_fragment() {
750749
let mut test_forge_project = MockForgeProject::files_from_string(
751750
"// src/index.tsx
752751
import ForgeUI, { render, Macro } from '@forge/ui';
@@ -769,10 +768,9 @@ fn correct_scopes_with_fragment() {
769768
.test_manifest
770769
.permissions
771770
.scopes
772-
.push("read:component:compass".into());
771+
.push("compass:atlassian-external".into());
773772

774773
let scan_result = scan_directory_test(test_forge_project);
775-
println!("scan_result {:#?}", scan_result);
776774
assert!(scan_result.contains_vulns(0))
777775
}
778776

@@ -887,6 +885,5 @@ fn authz_function_called_in_object_bitbucket() {
887885
);
888886

889887
let scan_result = scan_directory_test(test_forge_project);
890-
println!("scan_result {:#?}", scan_result);
891888
assert!(scan_result.contains_vulns(1))
892889
}

0 commit comments

Comments
 (0)