diff --git a/crates/forge_analyzer/src/checkers.rs b/crates/forge_analyzer/src/checkers.rs index e8ee115d..185b9bdc 100644 --- a/crates/forge_analyzer/src/checkers.rs +++ b/crates/forge_analyzer/src/checkers.rs @@ -1,17 +1,20 @@ use core::fmt; -use forge_permission_resolver::permissions_resolver::{check_url_for_permissions, RequestType}; +use forge_permission_resolver::permissions_resolver::{ + check_url_for_permissions, PermissionHashMap, RequestType, +}; use forge_utils::FxHashMap; use itertools::Itertools; +use regex::Regex; use smallvec::SmallVec; use std::{ cmp::max, + collections::HashMap, collections::HashSet, iter::{self, zip}, mem, ops::ControlFlow, path::PathBuf, }; - use tracing::{debug, info, warn}; use crate::interp::ProjectionVec; @@ -1094,6 +1097,10 @@ impl<'cx> Dataflow<'cx> for PermissionDataflow { let intrinsic_func_type = intrinsic_argument.name.unwrap(); let (resolver, regex_map) = match intrinsic_func_type { + IntrinsicName::RequestJiraAny => ( + interp.jira_any_permission_resolver, + interp.jira_any_regex_map, + ), IntrinsicName::RequestJiraSoftware => ( interp.jira_software_permission_resolver, interp.jira_software_regex_map, @@ -1113,7 +1120,9 @@ impl<'cx> Dataflow<'cx> for PermissionDataflow { interp.bitbucket_permission_resolver, interp.bitbucket_regex_map, ), - _ => unreachable!("Invalid intrinsic function type"), + IntrinsicName::Other => { + (&PermissionHashMap::new(), &HashMap::::new()) + } }; if intrinsic_argument.first_arg.is_none() { diff --git a/crates/forge_analyzer/src/definitions.rs b/crates/forge_analyzer/src/definitions.rs index 3acdee1b..b83750ae 100644 --- a/crates/forge_analyzer/src/definitions.rs +++ b/crates/forge_analyzer/src/definitions.rs @@ -623,6 +623,7 @@ enum LowerStage { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum IntrinsicName { + RequestJiraAny, RequestJiraSoftware, RequestJiraServiceManagement, RequestConfluence, @@ -997,6 +998,34 @@ impl FunctionAnalyzer<'_> { *prop == *"get" || *prop == *"getSecret" || *prop == *"query" } + fn resolve_jira_api_type(url: &str) -> Option { + // Pattern matching to classify, eg: api.[asApp | asUser]().requestJira(route`/rest/api/3/myself`); + match url { + // JSM requests + url if url.starts_with("/rest/servicedeskapi/") => { + Some(IntrinsicName::RequestJiraServiceManagement) + } + // Jira Software requests from https://developer.atlassian.com/cloud/jira/software/rest/intro/#introduction + url if url.starts_with("/rest/agile/") + || url.starts_with("/rest/devinfo/") + || url.starts_with("/rest/featureflags/") + || url.starts_with("/rest/deployments/") + || url.starts_with("/rest/builds") + || url.starts_with("/rest/remotelinks/") + || url.starts_with("/rest/security/") + || url.starts_with("/rest/operations/") + || url.starts_with("/rest/devopscomponents/") => + { + Some(IntrinsicName::RequestJiraSoftware) + } + // Jira requests, accept Jira API v2.0 or v3.0 + url if url.starts_with("/rest/api/2/") || url.starts_with("/rest/api/3/") => { + Some(IntrinsicName::RequestJira) + } + _ => None, + } + } + match *callee { [PropPath::Unknown((ref name, ..))] if *name == *"fetch" => Some(Intrinsic::Fetch), [PropPath::Def(def), ref authn @ .., PropPath::Static(ref last)] @@ -1006,15 +1035,29 @@ impl FunctionAnalyzer<'_> { && Some(&ImportKind::Default) == self.res.is_imported_from(def, "@forge/api") => { + let first_arg = first_arg?; + let is_as_app = authn.first() == Some(&PropPath::MemberCall("asApp".into())); + let function_name = if *last == "requestJira" { - IntrinsicName::RequestJira + // Resolve Jira API requests to either JSM/JS/Jira as all are bundled within requestJira() + match first_arg { + Expr::TaggedTpl(TaggedTpl { tpl, .. }) => { + tpl.quasis.first().map(|elem| &elem.raw) + } + Expr::Lit(Lit::Str(str_lit)) => Some(&str_lit.value), + _ => None, + } + .and_then(|atom| resolve_jira_api_type(atom.as_ref())) + .unwrap_or_else(|| { + warn!("Could not resolve Jira API type, falling back to any Jira request"); + IntrinsicName::RequestJiraAny + }) } else if *last == "requestBitbucket" { IntrinsicName::RequestBitbucket } else { IntrinsicName::RequestConfluence }; - let first_arg = first_arg?; - let is_as_app = authn.first() == Some(&PropPath::MemberCall("asApp".into())); + match classify_api_call(first_arg) { ApiCallKind::Unknown => { if is_as_app { diff --git a/crates/forge_analyzer/src/interp.rs b/crates/forge_analyzer/src/interp.rs index 58f02221..fe070bd4 100644 --- a/crates/forge_analyzer/src/interp.rs +++ b/crates/forge_analyzer/src/interp.rs @@ -390,11 +390,13 @@ pub struct Interp<'cx, C: Runner<'cx>> { pub callstack_arguments: Vec>, pub value_manager: ValueManager, pub permissions: Vec, + pub jira_any_permission_resolver: &'cx PermissionHashMap, pub jira_software_permission_resolver: &'cx PermissionHashMap, pub jira_service_management_permission_resolver: &'cx PermissionHashMap, pub jira_permission_resolver: &'cx PermissionHashMap, pub confluence_permission_resolver: &'cx PermissionHashMap, pub bitbucket_permission_resolver: &'cx PermissionHashMap, + pub jira_any_regex_map: &'cx HashMap, pub jira_software_regex_map: &'cx HashMap, pub jira_service_management_regex_map: &'cx HashMap, pub jira_regex_map: &'cx HashMap, @@ -512,6 +514,8 @@ impl<'cx, C: Runner<'cx>> Interp<'cx, C> { call_all: bool, call_uncalled: bool, permissions: Vec, + jira_any_permission_resolver: &'cx PermissionHashMap, + jira_any_regex_map: &'cx HashMap, jira_software_permission_resolver: &'cx PermissionHashMap, jira_software_regex_map: &'cx HashMap, jira_service_management_permission_resolver: &'cx PermissionHashMap, @@ -548,11 +552,13 @@ impl<'cx, C: Runner<'cx>> Interp<'cx, C> { expecting_value: VecDeque::default(), }, permissions, + jira_any_permission_resolver, jira_software_permission_resolver, jira_service_management_permission_resolver, jira_permission_resolver, confluence_permission_resolver, bitbucket_permission_resolver, + jira_any_regex_map, jira_software_regex_map, jira_service_management_regex_map, jira_regex_map, diff --git a/crates/forge_permission_resolver/src/permissions_resolver.rs b/crates/forge_permission_resolver/src/permissions_resolver.rs index 62466ba5..5da8af4d 100644 --- a/crates/forge_permission_resolver/src/permissions_resolver.rs +++ b/crates/forge_permission_resolver/src/permissions_resolver.rs @@ -95,6 +95,26 @@ pub fn check_url_for_permissions( vec![] } +pub fn get_permission_resolver_jira_any() -> (PermissionHashMap, HashMap) { + // Combine all Jira variations to achieve a generic "any" Jira + let (jira_map, jira_regex) = get_permission_resolver_jira(); + let (jsm_map, jsm_regex) = get_permission_resolver_jira_service_management(); + let (js_map, js_regex) = get_permission_resolver_jira_software(); + + let mut combined_permission_map = PermissionHashMap::default(); + let mut combined_regex_map = HashMap::default(); + + combined_permission_map.extend(jira_map); + combined_permission_map.extend(jsm_map); + combined_permission_map.extend(js_map); + + combined_regex_map.extend(jira_regex); + combined_regex_map.extend(jsm_regex); + combined_regex_map.extend(js_regex); + + (combined_permission_map, combined_regex_map) +} + pub fn get_permission_resolver_jira_software() -> (PermissionHashMap, HashMap) { let jira_software_url = "https://developer.atlassian.com/cloud/jira/software/swagger.v3.json"; get_permission_resolver(jira_software_url) diff --git a/crates/fsrt/src/main.rs b/crates/fsrt/src/main.rs index 76e7102e..53cd3e27 100644 --- a/crates/fsrt/src/main.rs +++ b/crates/fsrt/src/main.rs @@ -7,8 +7,8 @@ mod test; use clap::{Parser, ValueHint}; use forge_permission_resolver::permissions_resolver::{ get_permission_resolver_bitbucket, get_permission_resolver_confluence, - get_permission_resolver_jira, get_permission_resolver_jira_service_management, - get_permission_resolver_jira_software, + get_permission_resolver_jira, get_permission_resolver_jira_any, + get_permission_resolver_jira_service_management, get_permission_resolver_jira_software, }; use glob::glob; use std::{ @@ -410,6 +410,7 @@ pub(crate) fn scan_directory<'a>( let permissions = permissions_declared.into_iter().collect::>(); + let (jira_any_permission_resolver, jira_any_regex_map) = get_permission_resolver_jira_any(); let (jira_software_permission_resolver, jira_software_regex_map) = get_permission_resolver_jira_software(); let (jira_service_management_permission_resolver, jira_service_management_regex_map) = @@ -424,6 +425,8 @@ pub(crate) fn scan_directory<'a>( false, true, permissions.clone(), + &jira_any_permission_resolver, + &jira_any_regex_map, &jira_software_permission_resolver, &jira_software_regex_map, &jira_service_management_permission_resolver, @@ -441,6 +444,8 @@ pub(crate) fn scan_directory<'a>( false, false, permissions.clone(), + &jira_any_permission_resolver, + &jira_any_regex_map, &jira_software_permission_resolver, &jira_software_regex_map, &jira_service_management_permission_resolver, @@ -457,6 +462,8 @@ pub(crate) fn scan_directory<'a>( false, false, permissions.clone(), + &jira_any_permission_resolver, + &jira_any_regex_map, &jira_software_permission_resolver, &jira_software_regex_map, &jira_service_management_permission_resolver, @@ -475,6 +482,8 @@ pub(crate) fn scan_directory<'a>( false, false, permissions.clone(), + &jira_any_permission_resolver, + &jira_any_regex_map, &jira_software_permission_resolver, &jira_software_regex_map, &jira_service_management_permission_resolver, @@ -493,6 +502,8 @@ pub(crate) fn scan_directory<'a>( false, true, permissions, + &jira_any_permission_resolver, + &jira_any_regex_map, &jira_software_permission_resolver, &jira_software_regex_map, &jira_service_management_permission_resolver, diff --git a/crates/fsrt/src/test.rs b/crates/fsrt/src/test.rs index a628e733..a9cf7fce 100644 --- a/crates/fsrt/src/test.rs +++ b/crates/fsrt/src/test.rs @@ -601,7 +601,7 @@ fn basic_authz_vuln() { function getText({ text }) { - api.asApp().requestJira(route`rest/api/3/issue`); + api.asApp().requestJira(route`/rest/api/3/issue`); return 'Hello, world!\n' + text; } @@ -784,7 +784,7 @@ fn rovo_function_basic_authz_vuln() { function getText({ text }) { - api.asApp().requestJira(route`rest/api/3/issue`); + api.asApp().requestJira(route`/rest/api/3/issue`); return 'Hello, world!\n' + text; } diff --git a/test-apps/issue-1-resolver-with-vuln/src/index.js b/test-apps/issue-1-resolver-with-vuln/src/index.js index aca7752e..28cd83bc 100644 --- a/test-apps/issue-1-resolver-with-vuln/src/index.js +++ b/test-apps/issue-1-resolver-with-vuln/src/index.js @@ -4,7 +4,7 @@ import api, { route } from '@forge/api'; // src/lib/get-text.ts function getText({ text }) { - api.asApp().requestJira(route`rest/api/3/issue`); + api.asApp().requestJira(route`/rest/api/3/issue`); return 'Hello, world!\n' + text; }