Skip to content
26 changes: 26 additions & 0 deletions crates/forge_analyzer/src/reporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub struct Report {
ended_at: OffsetDateTime,
scanned: Vec<String>,
errors: bool,
error_message: String,
}

pub struct Reporter {
Expand Down Expand Up @@ -106,6 +107,7 @@ impl Reporter {
ended_at: OffsetDateTime::now_utc(),
scanned: self.apps.into_iter().map(|(key, _)| key).collect(),
errors: false,
error_message: String::new(),
}
}

Expand All @@ -115,10 +117,34 @@ impl Reporter {
}

impl Report {
#[inline]
pub fn error(error_message: String, scanned: Vec<String>) -> Self {
let now = OffsetDateTime::now_utc();
Self {
vulns: Vec::new(),
scanner: "FSRT",
started_at: now,
ended_at: now,
scanned,
errors: true,
error_message,
}
}

#[inline]
pub fn into_vulns(&self) -> &[Vulnerability] {
&self.vulns
}

#[inline]
pub fn has_errors(&self) -> bool {
self.errors
}

#[inline]
pub fn error_message(&self) -> &str {
&self.error_message
}
}

impl Default for Reporter {
Expand Down
55 changes: 42 additions & 13 deletions crates/forge_loader/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,11 @@ struct RawTrigger<'a> {
// maps to Trigger under Common Modules
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
struct EventTrigger<'a> {
#[serde(flatten, borrow)]
raw: RawTrigger<'a>,
key: &'a str,
#[serde(default, borrow)]
function: Option<&'a str>,
#[serde(default, borrow)]
endpoint: Option<&'a str>,
#[serde(borrow)]
events: Vec<&'a str>,
}
Expand Down Expand Up @@ -184,18 +187,42 @@ struct ContentByLineItem<'a> {
dynamic_properties: JustFunc<'a>,
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Copy)]
#[serde(untagged)]
#[serde(bound(deserialize = "'de: 'a"))]
enum MacroConfig<'a> {
Enabled(bool),
Object(JustFunc<'a>),
}

impl<'a> HasFunctions<'a> for MacroConfig<'a> {
fn append_functions<I: Extend<&'a str>>(&self, funcs: &mut I) {
if let Self::Object(config) = self {
config.append_functions(funcs);
}
}
}

#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize, Copy)]
pub struct MacroMod<'a> {
#[serde(flatten, borrow)]
common_keys: CommonKey<'a>,
config: Option<JustFunc<'a>>,
config: Option<MacroConfig<'a>>,
export: Option<JustFunc<'a>>,
}

// Jira Modules
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Copy)]
#[serde(untagged)]
#[serde(bound(deserialize = "'de: 'a"))]
enum LocalizedText<'a> {
Text(&'a str),
I18n { i18n: &'a str },
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Copy)]
pub struct JiraAdminPage<'a> {
title: &'a str,
title: LocalizedText<'a>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just delete this, since we only care about deserializing the function.

@gladstone-9 gladstone-9 Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will introduce a more permissive model (either deleting or making fields optional) in another PR

Out of scope of this PR

#[serde(flatten, borrow)]
common_keys: CommonKey<'a>,
}
Expand Down Expand Up @@ -267,15 +294,15 @@ struct AssetsImportType<'a> {
pub struct RovoAgent<'a> {
pub key: &'a str,
pub name: &'a str,
pub description: Option<&'a str>,
pub description: Option<String>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these fields are unused, we can just remove them as well.

@gladstone-9 gladstone-9 Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will introduce a more permissive model (either deleting or making fields optional) in another PR

Out of scope of this PR

pub icon: Option<&'a str>,
pub prompt: String, // as may be multiline
#[serde(default, rename = "conversationStarters", borrow)]
pub conversation_starters: Vec<&'a str>,
#[serde(default, rename = "conversationStarters")]
pub conversation_starters: Vec<String>,
#[serde(default, borrow)]
pub actions: Vec<&'a str>,
#[serde(rename = "followUpPrompt")]
pub follow_up_prompt: Option<&'a str>,
pub follow_up_prompt: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
Expand Down Expand Up @@ -638,10 +665,10 @@ pub struct Module<'a> {
extra: FxHashMap<String, serde_yaml::Value>,
}

#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Resource<'a> {
pub key: &'a str,
pub path: &'a str,
pub path: String,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be an owned String?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be another fix, but my understanding is that owned Strings allows serde to deserialize special characters.

This fix was introduced from failed on resources[4].path: invalid type: string ""static\\issue-activity\\build"" where

pub resources: Vec<Resource<'a>> relies on

pub struct Resource<'a> {
    pub key: &'a str,
    pub path: String,
}

so it needs a way to deserialize the special characters ie. \\

}

/// The representation of a Forge app's `manifest.yml`
Expand All @@ -655,7 +682,7 @@ pub struct ForgeManifest<'a> {
pub app: AppInfo<'a>,
#[serde(borrow)]
pub modules: ForgeModules<'a>,
#[serde(borrow)]
#[serde(default, borrow)]
pub permissions: Perms<'a>,
pub remotes: Option<Vec<Remotes>>,
#[serde(default, borrow)]
Expand Down Expand Up @@ -1169,9 +1196,11 @@ mod tests {
if let Some(string) = resolver.function {
assert_eq!(string, "Catch-me-if-you-can1");
}
if let Some(justfunc) = manifest.modules.macros[0].config {
if let Some(MacroConfig::Object(justfunc)) = manifest.modules.macros[0].config {
let func = justfunc.function.unwrap();
assert_eq!(func, "Catch-me-if-you-can2");
} else {
panic!("No config function found")
}

if let Some(justfunc) = manifest.modules.macros[0].export {
Expand Down Expand Up @@ -1327,7 +1356,7 @@ mod tests {
let agent = &manifest.modules.rovo_agent[0];
assert_eq!(agent.key, "data-discoverability");
assert_eq!(agent.name, "Data Discoverability");
assert_eq!(agent.description, Some("Test description"));
assert_eq!(agent.description.as_deref(), Some("Test description"));
assert_eq!(
agent.prompt,
"You are a helpful assistant that helps users manage their project risks. \nYou can retrieve risks from the risk register, create new risks and update existing ones."
Expand Down
8 changes: 4 additions & 4 deletions crates/fsrt/src/forge_project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use forge_analyzer::ctx::AppCtx;
use forge_analyzer::definitions::{Environment, PackageData, run_resolver};
use forge_loader::manifest::{Entrypoint, ForgeManifest, Resolved};
use forge_permission_resolver::permissions_resolver::PermMap;
use serde_yaml::Error as YamlError;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
Expand Down Expand Up @@ -78,7 +79,7 @@ pub(crate) trait ForgeProjectTrait<'a> {
#[allow(dead_code)]
fn get_secret_packages(&self) -> Vec<PackageData>;

fn get_manifest(&self) -> ForgeManifest<'_>;
fn get_manifest(&self) -> Result<ForgeManifest<'_>, YamlError>;
}
pub(crate) struct ForgeProject<'a> {
#[allow(dead_code)]
Expand Down Expand Up @@ -135,8 +136,7 @@ impl ForgeProjectTrait<'_> for ForgeProjectFromDir {
}
}

fn get_manifest(&self) -> ForgeManifest<'_> {
let out = serde_yaml::from_str(&self.manifest_file_content);
out.unwrap_or_default()
fn get_manifest(&self) -> Result<ForgeManifest<'_>, YamlError> {
serde_yaml::from_str(&self.manifest_file_content)
}
}
14 changes: 13 additions & 1 deletion crates/fsrt/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,19 @@ pub(crate) fn scan_directory<'a>(
secret_packages: &[PackageData],
) -> Result<Report> {
let paths = project.get_paths();
let manifest = project.get_manifest();
let manifest = match project.get_manifest() {
Ok(manifest) => manifest,
Err(err) => {
let error_message = format!(
"Could not process manifest, failed on {err}, submit an issue if you believe this is a bug --> https://github.com/atlassian-labs/FSRT/issues"
);
warn!("{error_message}");
return Ok(Report::error(
error_message,
vec![opts.appkey.clone().unwrap_or_default()],
));
}
};
let requested_permissions = manifest.permissions;
let permissions_declared = requested_permissions
.scopes
Expand Down
45 changes: 26 additions & 19 deletions crates/fsrt/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ impl ReportExt for Report {
pub(crate) struct MockForgeProject<'a> {
pub files_name_to_source: HashMap<PathBuf, Arc<SourceFile>>,
pub test_manifest: ForgeManifest<'a>,
pub manifest_file_content: Option<String>,
pub cm: Lrc<SourceMap>,
}

Expand All @@ -121,28 +122,28 @@ impl<'a> MockForgeProject<'a> {
pub fn files_from_string(string: &'a str) -> Self {
let different_files = string.split("//").filter(|file| !file.is_empty());

let manifest = if let Some(manifest_string) = different_files.clone().find(|string| {
string
.replace("//", "")
.trim_start()
.starts_with("manifest.yaml")
|| string
.trim_start()
let manifest_file_content = different_files
.clone()
.find(|string| {
string
.replace("//", "")
.starts_with("manifest.yml")
}) {
serde_yaml::from_str(manifest_string.split_once('\n').unwrap().1).unwrap_or_default()
} else {
ForgeManifest::create_manifest_with_func_mod(FunctionMod {
key: "main",
handler: "index.run",
providers: None,
.trim_start()
.starts_with("manifest.yaml")
|| string
.trim_start()
.replace("//", "")
.starts_with("manifest.yml")
})
};
.map(|manifest_string| manifest_string.split_once('\n').unwrap().1.to_string());

let mut mock_forge_project = MockForgeProject {
files_name_to_source: HashMap::new(),
test_manifest: manifest.to_owned(),
test_manifest: ForgeManifest::create_manifest_with_func_mod(FunctionMod {
key: "main",
handler: "index.run",
providers: None,
}),
manifest_file_content,
cm: Arc::default(),
};

Expand Down Expand Up @@ -186,8 +187,12 @@ impl<'a> ForgeProjectTrait<'a> for MockForgeProject<'a> {
vec![]
}

fn get_manifest(&self) -> ForgeManifest<'_> {
self.test_manifest.clone()
fn get_manifest(&self) -> Result<ForgeManifest<'_>, serde_yaml::Error> {
if let Some(manifest_file_content) = &self.manifest_file_content {
serde_yaml::from_str(manifest_file_content)
} else {
Ok(self.test_manifest.clone())
}
}
}

Expand Down Expand Up @@ -224,6 +229,7 @@ fn test_simple() {
let mut test_forge_project = MockForgeProject {
test_manifest: forge_manifest,
files_name_to_source: HashMap::new(),
manifest_file_content: None,
cm: Lrc::new(SourceMap::default()),
};
test_forge_project.add_file(
Expand All @@ -248,6 +254,7 @@ fn test_secret_vuln() {
let mut test_forge_project = MockForgeProject {
test_manifest: forge_manifest,
files_name_to_source: HashMap::new(),
manifest_file_content: None,
cm: Lrc::new(SourceMap::default()),
};
test_forge_project.add_file(
Expand Down
Loading