Skip to content

Commit 4969d11

Browse files
Classify Jira API requests with URL pattern matching, test cases fix and handle existing bug
1 parent cc5fdf3 commit 4969d11

File tree

7 files changed

+100
-11
lines changed

7 files changed

+100
-11
lines changed

crates/forge_analyzer/src/checkers.rs

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
use core::fmt;
2-
use forge_permission_resolver::permissions_resolver::{check_url_for_permissions, RequestType};
2+
use forge_permission_resolver::permissions_resolver::{
3+
check_url_for_permissions, PermissionHashMap, RequestType,
4+
};
35
use forge_utils::FxHashMap;
46
use itertools::Itertools;
7+
use regex::Regex;
58
use smallvec::SmallVec;
69
use std::{
710
cmp::max,
11+
collections::HashMap,
812
collections::HashSet,
913
iter::{self, zip},
1014
mem,
1115
ops::ControlFlow,
1216
path::PathBuf,
1317
};
14-
1518
use tracing::{debug, info, warn};
1619

1720
use crate::interp::ProjectionVec;
@@ -1094,6 +1097,10 @@ impl<'cx> Dataflow<'cx> for PermissionDataflow {
10941097
let intrinsic_func_type = intrinsic_argument.name.unwrap();
10951098

10961099
let (resolver, regex_map) = match intrinsic_func_type {
1100+
IntrinsicName::RequestJiraAny => (
1101+
interp.jira_any_permission_resolver,
1102+
interp.jira_any_regex_map,
1103+
),
10971104
IntrinsicName::RequestJiraSoftware => (
10981105
interp.jira_software_permission_resolver,
10991106
interp.jira_software_regex_map,
@@ -1113,7 +1120,9 @@ impl<'cx> Dataflow<'cx> for PermissionDataflow {
11131120
interp.bitbucket_permission_resolver,
11141121
interp.bitbucket_regex_map,
11151122
),
1116-
_ => unreachable!("Invalid intrinsic function type"),
1123+
IntrinsicName::Other => {
1124+
(&PermissionHashMap::new(), &HashMap::<String, Regex>::new())
1125+
}
11171126
};
11181127

11191128
if intrinsic_argument.first_arg.is_none() {

crates/forge_analyzer/src/definitions.rs

+46-3
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,7 @@ enum LowerStage {
623623

624624
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
625625
pub enum IntrinsicName {
626+
RequestJiraAny,
626627
RequestJiraSoftware,
627628
RequestJiraServiceManagement,
628629
RequestConfluence,
@@ -997,6 +998,34 @@ impl FunctionAnalyzer<'_> {
997998
*prop == *"get" || *prop == *"getSecret" || *prop == *"query"
998999
}
9991000

1001+
fn resolve_jira_api_type(url: &str) -> Option<IntrinsicName> {
1002+
// Pattern matching to classify, eg: api.[asApp | asUser]().requestJira(route`/rest/api/3/myself`);
1003+
match url {
1004+
// JSM requests
1005+
url if url.starts_with("/rest/servicedeskapi/") => {
1006+
Some(IntrinsicName::RequestJiraServiceManagement)
1007+
}
1008+
// Jira Software requests from https://developer.atlassian.com/cloud/jira/software/rest/intro/#introduction
1009+
url if url.starts_with("/rest/agile/")
1010+
|| url.starts_with("/rest/devinfo/")
1011+
|| url.starts_with("/rest/featureflags/")
1012+
|| url.starts_with("/rest/deployments/")
1013+
|| url.starts_with("/rest/builds")
1014+
|| url.starts_with("/rest/remotelinks/")
1015+
|| url.starts_with("/rest/security/")
1016+
|| url.starts_with("/rest/operations/")
1017+
|| url.starts_with("/rest/devopscomponents/") =>
1018+
{
1019+
Some(IntrinsicName::RequestJiraSoftware)
1020+
}
1021+
// Jira requests, accept Jira API v2.0 or v3.0
1022+
url if url.starts_with("/rest/api/2/") || url.starts_with("/rest/api/3/") => {
1023+
Some(IntrinsicName::RequestJira)
1024+
}
1025+
_ => None,
1026+
}
1027+
}
1028+
10001029
match *callee {
10011030
[PropPath::Unknown((ref name, ..))] if *name == *"fetch" => Some(Intrinsic::Fetch),
10021031
[PropPath::Def(def), ref authn @ .., PropPath::Static(ref last)]
@@ -1006,15 +1035,29 @@ impl FunctionAnalyzer<'_> {
10061035
&& Some(&ImportKind::Default)
10071036
== self.res.is_imported_from(def, "@forge/api") =>
10081037
{
1038+
let first_arg = first_arg?;
1039+
let is_as_app = authn.first() == Some(&PropPath::MemberCall("asApp".into()));
1040+
10091041
let function_name = if *last == "requestJira" {
1010-
IntrinsicName::RequestJira
1042+
// Resolve Jira API requests to either JSM/JS/Jira as all are bundled within requestJira()
1043+
match first_arg {
1044+
Expr::TaggedTpl(TaggedTpl { tpl, .. }) => {
1045+
tpl.quasis.first().map(|elem| &elem.raw)
1046+
}
1047+
Expr::Lit(Lit::Str(str_lit)) => Some(&str_lit.value),
1048+
_ => None,
1049+
}
1050+
.and_then(|atom| resolve_jira_api_type(atom.as_ref()))
1051+
.unwrap_or_else(|| {
1052+
warn!("Could not resolve Jira API type, falling back to any Jira request");
1053+
IntrinsicName::RequestJiraAny
1054+
})
10111055
} else if *last == "requestBitbucket" {
10121056
IntrinsicName::RequestBitbucket
10131057
} else {
10141058
IntrinsicName::RequestConfluence
10151059
};
1016-
let first_arg = first_arg?;
1017-
let is_as_app = authn.first() == Some(&PropPath::MemberCall("asApp".into()));
1060+
10181061
match classify_api_call(first_arg) {
10191062
ApiCallKind::Unknown => {
10201063
if is_as_app {

crates/forge_analyzer/src/interp.rs

+6
Original file line numberDiff line numberDiff line change
@@ -390,11 +390,13 @@ pub struct Interp<'cx, C: Runner<'cx>> {
390390
pub callstack_arguments: Vec<Vec<Value>>,
391391
pub value_manager: ValueManager,
392392
pub permissions: Vec<String>,
393+
pub jira_any_permission_resolver: &'cx PermissionHashMap,
393394
pub jira_software_permission_resolver: &'cx PermissionHashMap,
394395
pub jira_service_management_permission_resolver: &'cx PermissionHashMap,
395396
pub jira_permission_resolver: &'cx PermissionHashMap,
396397
pub confluence_permission_resolver: &'cx PermissionHashMap,
397398
pub bitbucket_permission_resolver: &'cx PermissionHashMap,
399+
pub jira_any_regex_map: &'cx HashMap<String, Regex>,
398400
pub jira_software_regex_map: &'cx HashMap<String, Regex>,
399401
pub jira_service_management_regex_map: &'cx HashMap<String, Regex>,
400402
pub jira_regex_map: &'cx HashMap<String, Regex>,
@@ -512,6 +514,8 @@ impl<'cx, C: Runner<'cx>> Interp<'cx, C> {
512514
call_all: bool,
513515
call_uncalled: bool,
514516
permissions: Vec<String>,
517+
jira_any_permission_resolver: &'cx PermissionHashMap,
518+
jira_any_regex_map: &'cx HashMap<String, Regex>,
515519
jira_software_permission_resolver: &'cx PermissionHashMap,
516520
jira_software_regex_map: &'cx HashMap<String, Regex>,
517521
jira_service_management_permission_resolver: &'cx PermissionHashMap,
@@ -548,11 +552,13 @@ impl<'cx, C: Runner<'cx>> Interp<'cx, C> {
548552
expecting_value: VecDeque::default(),
549553
},
550554
permissions,
555+
jira_any_permission_resolver,
551556
jira_software_permission_resolver,
552557
jira_service_management_permission_resolver,
553558
jira_permission_resolver,
554559
confluence_permission_resolver,
555560
bitbucket_permission_resolver,
561+
jira_any_regex_map,
556562
jira_software_regex_map,
557563
jira_service_management_regex_map,
558564
jira_regex_map,

crates/forge_permission_resolver/src/permissions_resolver.rs

+20
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,26 @@ pub fn check_url_for_permissions(
9595
vec![]
9696
}
9797

98+
pub fn get_permission_resolver_jira_any() -> (PermissionHashMap, HashMap<String, Regex>) {
99+
// Combine all Jira variations to achieve a generic "any" Jira
100+
let (jira_map, jira_regex) = get_permission_resolver_jira();
101+
let (jsm_map, jsm_regex) = get_permission_resolver_jira_service_management();
102+
let (js_map, js_regex) = get_permission_resolver_jira_software();
103+
104+
let mut combined_permission_map = PermissionHashMap::default();
105+
let mut combined_regex_map = HashMap::default();
106+
107+
combined_permission_map.extend(jira_map);
108+
combined_permission_map.extend(jsm_map);
109+
combined_permission_map.extend(js_map);
110+
111+
combined_regex_map.extend(jira_regex);
112+
combined_regex_map.extend(jsm_regex);
113+
combined_regex_map.extend(js_regex);
114+
115+
(combined_permission_map, combined_regex_map)
116+
}
117+
98118
pub fn get_permission_resolver_jira_software() -> (PermissionHashMap, HashMap<String, Regex>) {
99119
let jira_software_url = "https://developer.atlassian.com/cloud/jira/software/swagger.v3.json";
100120
get_permission_resolver(jira_software_url)

crates/fsrt/src/main.rs

+13-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ mod test;
77
use clap::{Parser, ValueHint};
88
use forge_permission_resolver::permissions_resolver::{
99
get_permission_resolver_bitbucket, get_permission_resolver_confluence,
10-
get_permission_resolver_jira, get_permission_resolver_jira_service_management,
11-
get_permission_resolver_jira_software,
10+
get_permission_resolver_jira, get_permission_resolver_jira_any,
11+
get_permission_resolver_jira_service_management, get_permission_resolver_jira_software,
1212
};
1313
use glob::glob;
1414
use std::{
@@ -410,6 +410,7 @@ pub(crate) fn scan_directory<'a>(
410410

411411
let permissions = permissions_declared.into_iter().collect::<Vec<_>>();
412412

413+
let (jira_any_permission_resolver, jira_any_regex_map) = get_permission_resolver_jira_any();
413414
let (jira_software_permission_resolver, jira_software_regex_map) =
414415
get_permission_resolver_jira_software();
415416
let (jira_service_management_permission_resolver, jira_service_management_regex_map) =
@@ -424,6 +425,8 @@ pub(crate) fn scan_directory<'a>(
424425
false,
425426
true,
426427
permissions.clone(),
428+
&jira_any_permission_resolver,
429+
&jira_any_regex_map,
427430
&jira_software_permission_resolver,
428431
&jira_software_regex_map,
429432
&jira_service_management_permission_resolver,
@@ -441,6 +444,8 @@ pub(crate) fn scan_directory<'a>(
441444
false,
442445
false,
443446
permissions.clone(),
447+
&jira_any_permission_resolver,
448+
&jira_any_regex_map,
444449
&jira_software_permission_resolver,
445450
&jira_software_regex_map,
446451
&jira_service_management_permission_resolver,
@@ -457,6 +462,8 @@ pub(crate) fn scan_directory<'a>(
457462
false,
458463
false,
459464
permissions.clone(),
465+
&jira_any_permission_resolver,
466+
&jira_any_regex_map,
460467
&jira_software_permission_resolver,
461468
&jira_software_regex_map,
462469
&jira_service_management_permission_resolver,
@@ -475,6 +482,8 @@ pub(crate) fn scan_directory<'a>(
475482
false,
476483
false,
477484
permissions.clone(),
485+
&jira_any_permission_resolver,
486+
&jira_any_regex_map,
478487
&jira_software_permission_resolver,
479488
&jira_software_regex_map,
480489
&jira_service_management_permission_resolver,
@@ -493,6 +502,8 @@ pub(crate) fn scan_directory<'a>(
493502
false,
494503
true,
495504
permissions,
505+
&jira_any_permission_resolver,
506+
&jira_any_regex_map,
496507
&jira_software_permission_resolver,
497508
&jira_software_regex_map,
498509
&jira_service_management_permission_resolver,

crates/fsrt/src/test.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ fn basic_authz_vuln() {
601601
602602
603603
function getText({ text }) {
604-
api.asApp().requestJira(route`rest/api/3/issue`);
604+
api.asApp().requestJira(route`/rest/api/3/issue`);
605605
return 'Hello, world!\n' + text;
606606
}
607607
@@ -784,7 +784,7 @@ fn rovo_function_basic_authz_vuln() {
784784
785785
786786
function getText({ text }) {
787-
api.asApp().requestJira(route`rest/api/3/issue`);
787+
api.asApp().requestJira(route`/rest/api/3/issue`);
788788
return 'Hello, world!\n' + text;
789789
}
790790

test-apps/issue-1-resolver-with-vuln/src/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import api, { route } from '@forge/api';
44

55
// src/lib/get-text.ts
66
function getText({ text }) {
7-
api.asApp().requestJira(route`rest/api/3/issue`);
7+
api.asApp().requestJira(route`/rest/api/3/issue`);
88
return 'Hello, world!\n' + text;
99
}
1010

0 commit comments

Comments
 (0)