fix(integrations): security and correctness improvements
All checks were successful
Test / rust-fmt-check (pull_request) Successful in 12s
Test / frontend-typecheck (pull_request) Successful in 1m18s
Test / frontend-tests (pull_request) Successful in 1m21s
Test / rust-clippy (pull_request) Successful in 3m56s
PR Review Automation / review (pull_request) Successful in 4m20s
Test / rust-tests (pull_request) Successful in 5m22s
All checks were successful
Test / rust-fmt-check (pull_request) Successful in 12s
Test / frontend-typecheck (pull_request) Successful in 1m18s
Test / frontend-tests (pull_request) Successful in 1m21s
Test / rust-clippy (pull_request) Successful in 3m56s
PR Review Automation / review (pull_request) Successful in 4m20s
Test / rust-tests (pull_request) Successful in 5m22s
- Add url canonicalization for deduplication (strip fragments/query params) - Add WIQL injection escaping for Azure DevOps work item searches - Add CQL injection escaping for Confluence searches - Add MAX_EXPANDED_QUERIES constant for consistency - Fix logging to show expanded_query instead of search_url - Add input validation for empty queries - Add url crate dependency for URL parsing All 142 tests pass.
This commit is contained in:
parent
096068ed2b
commit
5b45c6c418
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@ -6174,6 +6174,7 @@ dependencies = [
|
|||||||
"tokio-test",
|
"tokio-test",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"url",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
"warp",
|
"warp",
|
||||||
|
|||||||
@ -44,6 +44,7 @@ lazy_static = "1.4"
|
|||||||
warp = "0.3"
|
warp = "0.3"
|
||||||
urlencoding = "2"
|
urlencoding = "2"
|
||||||
infer = "0.15"
|
infer = "0.15"
|
||||||
|
url = "2.5.8"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
use super::confluence_search::SearchResult;
|
use super::confluence_search::SearchResult;
|
||||||
use crate::integrations::query_expansion::expand_query;
|
use crate::integrations::query_expansion::expand_query;
|
||||||
|
|
||||||
|
const MAX_EXPANDED_QUERIES: usize = 3;
|
||||||
|
|
||||||
|
fn escape_wiql(s: &str) -> String {
|
||||||
|
s.replace('\'', "''")
|
||||||
|
}
|
||||||
|
|
||||||
/// Search Azure DevOps Wiki for content matching the query
|
/// Search Azure DevOps Wiki for content matching the query
|
||||||
pub async fn search_wiki(
|
pub async fn search_wiki(
|
||||||
org_url: &str,
|
org_url: &str,
|
||||||
@ -15,7 +21,7 @@ pub async fn search_wiki(
|
|||||||
|
|
||||||
let mut all_results = Vec::new();
|
let mut all_results = Vec::new();
|
||||||
|
|
||||||
for expanded_query in expanded_queries.iter().take(3) {
|
for expanded_query in expanded_queries.iter().take(MAX_EXPANDED_QUERIES) {
|
||||||
// Use Azure DevOps Search API
|
// Use Azure DevOps Search API
|
||||||
let search_url = format!(
|
let search_url = format!(
|
||||||
"{}/_apis/search/wikisearchresults?api-version=7.0",
|
"{}/_apis/search/wikisearchresults?api-version=7.0",
|
||||||
@ -30,10 +36,7 @@ pub async fn search_wiki(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!("Searching Azure DevOps Wiki with query: {}", expanded_query);
|
||||||
"Searching Azure DevOps Wiki with expanded query: {}",
|
|
||||||
search_url
|
|
||||||
);
|
|
||||||
|
|
||||||
let resp = client
|
let resp = client
|
||||||
.post(&search_url)
|
.post(&search_url)
|
||||||
@ -58,7 +61,7 @@ pub async fn search_wiki(
|
|||||||
.map_err(|e| format!("Failed to parse ADO wiki search response: {e}"))?;
|
.map_err(|e| format!("Failed to parse ADO wiki search response: {e}"))?;
|
||||||
|
|
||||||
if let Some(results_array) = json["results"].as_array() {
|
if let Some(results_array) = json["results"].as_array() {
|
||||||
for item in results_array.iter().take(3) {
|
for item in results_array.iter().take(MAX_EXPANDED_QUERIES) {
|
||||||
let title = item["fileName"].as_str().unwrap_or("Untitled").to_string();
|
let title = item["fileName"].as_str().unwrap_or("Untitled").to_string();
|
||||||
|
|
||||||
let path = item["path"].as_str().unwrap_or("");
|
let path = item["path"].as_str().unwrap_or("");
|
||||||
@ -165,22 +168,26 @@ pub async fn search_work_items(
|
|||||||
|
|
||||||
let mut all_results = Vec::new();
|
let mut all_results = Vec::new();
|
||||||
|
|
||||||
for expanded_query in expanded_queries.iter().take(3) {
|
for expanded_query in expanded_queries.iter().take(MAX_EXPANDED_QUERIES) {
|
||||||
// Use WIQL (Work Item Query Language)
|
// Use WIQL (Work Item Query Language)
|
||||||
let wiql_url = format!(
|
let wiql_url = format!(
|
||||||
"{}/_apis/wit/wiql?api-version=7.0",
|
"{}/_apis/wit/wiql?api-version=7.0",
|
||||||
org_url.trim_end_matches('/')
|
org_url.trim_end_matches('/')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let safe_query = escape_wiql(expanded_query);
|
||||||
let wiql_query = format!(
|
let wiql_query = format!(
|
||||||
"SELECT [System.Id], [System.Title], [System.Description], [System.State] FROM WorkItems WHERE [System.TeamProject] = '{project}' AND ([System.Title] CONTAINS '{expanded_query}' OR [System.Description] CONTAINS '{expanded_query}') ORDER BY [System.ChangedDate] DESC"
|
"SELECT [System.Id], [System.Title], [System.Description], [System.State] FROM WorkItems WHERE [System.TeamProject] = '{project}' AND ([System.Title] CONTAINS '{safe_query}' OR [System.Description] CONTAINS '{safe_query}') ORDER BY [System.ChangedDate] DESC"
|
||||||
);
|
);
|
||||||
|
|
||||||
let wiql_body = serde_json::json!({
|
let wiql_body = serde_json::json!({
|
||||||
"query": wiql_query
|
"query": wiql_query
|
||||||
});
|
});
|
||||||
|
|
||||||
tracing::info!("Searching Azure DevOps work items with expanded query");
|
tracing::info!(
|
||||||
|
"Searching Azure DevOps work items with query: {}",
|
||||||
|
expanded_query
|
||||||
|
);
|
||||||
|
|
||||||
let resp = client
|
let resp = client
|
||||||
.post(&wiql_url)
|
.post(&wiql_url)
|
||||||
@ -203,7 +210,7 @@ pub async fn search_work_items(
|
|||||||
|
|
||||||
if let Some(work_items) = json["workItems"].as_array() {
|
if let Some(work_items) = json["workItems"].as_array() {
|
||||||
// Fetch details for top 3 work items
|
// Fetch details for top 3 work items
|
||||||
for item in work_items.iter().take(3) {
|
for item in work_items.iter().take(MAX_EXPANDED_QUERIES) {
|
||||||
if let Some(id) = item["id"].as_i64() {
|
if let Some(id) = item["id"].as_i64() {
|
||||||
if let Ok(work_item) =
|
if let Ok(work_item) =
|
||||||
fetch_work_item_details(org_url, id, &cookie_header).await
|
fetch_work_item_details(org_url, id, &cookie_header).await
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use super::query_expansion::expand_query;
|
use super::query_expansion::expand_query;
|
||||||
|
|
||||||
|
const MAX_EXPANDED_QUERIES: usize = 3;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SearchResult {
|
pub struct SearchResult {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@ -11,6 +14,25 @@ pub struct SearchResult {
|
|||||||
pub source: String,
|
pub source: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn canonicalize_url(url: &str) -> String {
|
||||||
|
Url::parse(url)
|
||||||
|
.ok()
|
||||||
|
.map(|u| {
|
||||||
|
let mut u = u.clone();
|
||||||
|
u.set_fragment(None);
|
||||||
|
u.set_query(None);
|
||||||
|
u.to_string()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| url.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape_cql(s: &str) -> String {
|
||||||
|
s.replace('"', "\\\"")
|
||||||
|
.replace(')', "\\)")
|
||||||
|
.replace('(', "\\(")
|
||||||
|
.replace('~', "\\~")
|
||||||
|
}
|
||||||
|
|
||||||
/// Search Confluence for content matching the query
|
/// Search Confluence for content matching the query
|
||||||
///
|
///
|
||||||
/// This function expands the user query with related terms, synonyms, and variations
|
/// This function expands the user query with related terms, synonyms, and variations
|
||||||
@ -27,11 +49,12 @@ pub async fn search_confluence(
|
|||||||
|
|
||||||
let mut all_results = Vec::new();
|
let mut all_results = Vec::new();
|
||||||
|
|
||||||
for expanded_query in expanded_queries.iter().take(3) {
|
for expanded_query in expanded_queries.iter().take(MAX_EXPANDED_QUERIES) {
|
||||||
|
let safe_query = escape_cql(expanded_query);
|
||||||
let search_url = format!(
|
let search_url = format!(
|
||||||
"{}/rest/api/search?cql=text~\"{}\"&limit=5",
|
"{}/rest/api/search?cql=text~\"{}\"&limit=5",
|
||||||
base_url.trim_end_matches('/'),
|
base_url.trim_end_matches('/'),
|
||||||
urlencoding::encode(expanded_query)
|
urlencoding::encode(&safe_query)
|
||||||
);
|
);
|
||||||
|
|
||||||
tracing::info!("Searching Confluence with expanded query: {}", search_url);
|
tracing::info!("Searching Confluence with expanded query: {}", search_url);
|
||||||
@ -100,8 +123,8 @@ pub async fn search_confluence(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
all_results.sort_by(|a, b| a.url.cmp(&b.url));
|
all_results.sort_by(|a, b| canonicalize_url(&a.url).cmp(&canonicalize_url(&b.url)));
|
||||||
all_results.dedup_by(|a, b| a.url == b.url);
|
all_results.dedup_by(|a, b| canonicalize_url(&a.url) == canonicalize_url(&b.url));
|
||||||
|
|
||||||
Ok(all_results)
|
Ok(all_results)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,6 +78,10 @@ fn get_product_synonyms(query: &str) -> Vec<String> {
|
|||||||
/// A vector of query strings to search, with the original query first
|
/// A vector of query strings to search, with the original query first
|
||||||
/// followed by expanded variations
|
/// followed by expanded variations
|
||||||
pub fn expand_query(query: &str) -> Vec<String> {
|
pub fn expand_query(query: &str) -> Vec<String> {
|
||||||
|
if query.trim().is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
let mut expanded = vec![query.to_string()];
|
let mut expanded = vec![query.to_string()];
|
||||||
|
|
||||||
// Get product synonyms
|
// Get product synonyms
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
use super::confluence_search::SearchResult;
|
use super::confluence_search::SearchResult;
|
||||||
use crate::integrations::query_expansion::expand_query;
|
use crate::integrations::query_expansion::expand_query;
|
||||||
|
|
||||||
|
const MAX_EXPANDED_QUERIES: usize = 3;
|
||||||
|
|
||||||
/// Search ServiceNow Knowledge Base for content matching the query
|
/// Search ServiceNow Knowledge Base for content matching the query
|
||||||
pub async fn search_servicenow(
|
pub async fn search_servicenow(
|
||||||
instance_url: &str,
|
instance_url: &str,
|
||||||
@ -14,7 +16,7 @@ pub async fn search_servicenow(
|
|||||||
|
|
||||||
let mut all_results = Vec::new();
|
let mut all_results = Vec::new();
|
||||||
|
|
||||||
for expanded_query in expanded_queries.iter().take(3) {
|
for expanded_query in expanded_queries.iter().take(MAX_EXPANDED_QUERIES) {
|
||||||
// Search Knowledge Base articles
|
// Search Knowledge Base articles
|
||||||
let search_url = format!(
|
let search_url = format!(
|
||||||
"{}/api/now/table/kb_knowledge?sysparm_query=textLIKE{}^ORshort_descriptionLIKE{}&sysparm_limit=5",
|
"{}/api/now/table/kb_knowledge?sysparm_query=textLIKE{}^ORshort_descriptionLIKE{}&sysparm_limit=5",
|
||||||
@ -23,7 +25,7 @@ pub async fn search_servicenow(
|
|||||||
urlencoding::encode(expanded_query)
|
urlencoding::encode(expanded_query)
|
||||||
);
|
);
|
||||||
|
|
||||||
tracing::info!("Searching ServiceNow with expanded query: {}", search_url);
|
tracing::info!("Searching ServiceNow with query: {}", expanded_query);
|
||||||
|
|
||||||
let resp = client
|
let resp = client
|
||||||
.get(&search_url)
|
.get(&search_url)
|
||||||
@ -46,7 +48,7 @@ pub async fn search_servicenow(
|
|||||||
.map_err(|e| format!("Failed to parse ServiceNow search response: {e}"))?;
|
.map_err(|e| format!("Failed to parse ServiceNow search response: {e}"))?;
|
||||||
|
|
||||||
if let Some(result_array) = json["result"].as_array() {
|
if let Some(result_array) = json["result"].as_array() {
|
||||||
for item in result_array.iter().take(3) {
|
for item in result_array.iter().take(MAX_EXPANDED_QUERIES) {
|
||||||
// Take top 3 results
|
// Take top 3 results
|
||||||
let title = item["short_description"]
|
let title = item["short_description"]
|
||||||
.as_str()
|
.as_str()
|
||||||
@ -107,7 +109,7 @@ pub async fn search_incidents(
|
|||||||
|
|
||||||
let mut all_results = Vec::new();
|
let mut all_results = Vec::new();
|
||||||
|
|
||||||
for expanded_query in expanded_queries.iter().take(3) {
|
for expanded_query in expanded_queries.iter().take(MAX_EXPANDED_QUERIES) {
|
||||||
// Search incidents
|
// Search incidents
|
||||||
let search_url = format!(
|
let search_url = format!(
|
||||||
"{}/api/now/table/incident?sysparm_query=short_descriptionLIKE{}^ORdescriptionLIKE{}&sysparm_limit=3&sysparm_display_value=true",
|
"{}/api/now/table/incident?sysparm_query=short_descriptionLIKE{}^ORdescriptionLIKE{}&sysparm_limit=3&sysparm_display_value=true",
|
||||||
@ -117,8 +119,8 @@ pub async fn search_incidents(
|
|||||||
);
|
);
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Searching ServiceNow incidents with expanded query: {}",
|
"Searching ServiceNow incidents with query: {}",
|
||||||
search_url
|
expanded_query
|
||||||
);
|
);
|
||||||
|
|
||||||
let resp = client
|
let resp = client
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user