-
Notifications
You must be signed in to change notification settings - Fork 2
fix: security and code quality improvements (SONAR:dallay_corvus) #144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
321b248
8d1aa61
dd93d94
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| //! portable AI identity. This module handles loading and converting AIEOS v1.1 | ||
| //! JSON to Corvus's system prompt format. | ||
|
|
||
| use std::path::PathBuf; | ||
| use crate::config::IdentityConfig; | ||
| use anyhow::{Context, Result}; | ||
| use serde::{Deserialize, Serialize}; | ||
|
|
@@ -166,13 +167,26 @@ pub fn load_aieos_identity( | |
|
|
||
| // Try aieos_path first | ||
| if let Some(ref path) = config.aieos_path { | ||
| let workspace_canonical = std::fs::canonicalize(workspace_dir) | ||
| .with_context(|| format!("Failed to canonicalize workspace directory: {}", workspace_dir.display()))?; | ||
|
|
||
| let full_path = if Path::new(path).is_absolute() { | ||
| PathBuf::from(path) | ||
| } else { | ||
| workspace_dir.join(path) | ||
| }; | ||
|
|
||
| let content = std::fs::read_to_string(&full_path) | ||
| let full_path_canonical = std::fs::canonicalize(&full_path) | ||
| .with_context(|| format!("Failed to canonicalize AIEOS path: {}", full_path.display()))?; | ||
|
|
||
| if !full_path_canonical.starts_with(&workspace_canonical) { | ||
| anyhow::bail!( | ||
| "Security error: AIEOS path {} is outside the workspace directory {}", | ||
| full_path.display(), | ||
| workspace_dir.display() | ||
| ); | ||
| } | ||
| let content = std::fs::read_to_string(&full_path_canonical) | ||
| .with_context(|| format!("Failed to read AIEOS file: {}", full_path.display()))?; | ||
|
Comment on lines
+170
to
190
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add regression tests for traversal and workspace-boundary enforcement. Lines [170-190] add important path hardening, but there’s no focused test coverage here for: (1) in-workspace success, (2) 🧪 Suggested test cases to add+ #[test]
+ fn load_aieos_identity_rejects_path_outside_workspace() {
+ // setup temp workspace and external file, configure aieos_path = "../outside.json"
+ // assert error contains "outside the workspace directory"
+ }
+
+ #[test]
+ fn load_aieos_identity_accepts_path_inside_workspace() {
+ // setup temp workspace with identity.json
+ // assert Ok(Some(...))
+ }
+
+ #[test]
+ fn load_aieos_identity_rejects_symlink_escape() {
+ // setup symlink inside workspace pointing outside
+ // assert error contains "outside the workspace directory"
+ }🤖 Prompt for AI Agents |
||
|
|
||
| let identity: AieosIdentity = serde_json::from_str(&content) | ||
|
|
@@ -206,8 +220,6 @@ pub fn load_aieos_identity( | |
| ) | ||
| } | ||
|
|
||
| use std::path::PathBuf; | ||
|
|
||
| /// Convert AIEOS identity to a system prompt string. | ||
| /// | ||
| /// Formats the AIEOS data into a structured markdown prompt compatible | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -214,7 +214,7 @@ impl BrowserTool { | |
| ) -> Self { | ||
| Self { | ||
| security, | ||
| allowed_domains: normalize_domains(allowed_domains), | ||
| allowed_domains: normalize_domains(allowed_domains).expect("Invalid allowlist entries"), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not panic on invalid allowlist during tool construction.
💡 Suggested fix- pub fn new(
+ pub fn new(
security: Arc<SecurityPolicy>,
allowed_domains: Vec<String>,
session_name: Option<String>,
- ) -> Self {
- Self::new_with_backend(
+ ) -> anyhow::Result<Self> {
+ Self::new_with_backend(
security,
allowed_domains,
session_name,
"agent_browser".into(),
true,
"http://127.0.0.1:9515".into(),
None,
ComputerUseConfig::default(),
)
}
@@
- ) -> Self {
- Self {
+ ) -> anyhow::Result<Self> {
+ let allowed_domains = normalize_domains(allowed_domains)
+ .context("Invalid browser.allowed_domains configuration")?;
+ Ok(Self {
security,
- allowed_domains: normalize_domains(allowed_domains).expect("Invalid allowlist entries"),
+ allowed_domains,
session_name,
backend,
native_headless,
native_webdriver_url,
native_chrome_path,
computer_use,
#[cfg(feature = "browser-native")]
native_state: tokio::sync::Mutex::new(native_backend::NativeBrowserState::default()),
- }
+ })
}🤖 Prompt for AI Agents |
||
| session_name, | ||
| backend, | ||
| native_headless, | ||
|
|
@@ -1933,12 +1933,14 @@ fn unavailable_action_for_backend_error(action: &str, backend: ResolvedBackend) | |
| ) | ||
| } | ||
|
|
||
| fn normalize_domains(domains: Vec<String>) -> Vec<String> { | ||
| domains | ||
| .into_iter() | ||
| .map(|d| d.trim().to_lowercase()) | ||
| .filter(|d| !d.is_empty()) | ||
| .collect() | ||
| fn normalize_domains(domains: Vec<String>) -> anyhow::Result<Vec<String>> { | ||
| let mut normalized = Vec::new(); | ||
| for domain in domains { | ||
| normalized.push(crate::tools::url_safety::normalize_domain(&domain)?); | ||
| } | ||
| normalized.sort_unstable(); | ||
| normalized.dedup(); | ||
| Ok(normalized) | ||
| } | ||
|
Comment on lines
+1936
to
1944
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Prefer the shared normalizer to avoid policy drift. This helper duplicates ♻️ Suggested simplification fn normalize_domains(domains: Vec<String>) -> anyhow::Result<Vec<String>> {
- let mut normalized = Vec::new();
- for domain in domains {
- normalized.push(crate::tools::url_safety::normalize_domain(&domain)?);
- }
- normalized.sort_unstable();
- normalized.dedup();
- Ok(normalized)
+ crate::tools::url_safety::normalize_allowed_domains(domains)
}🤖 Prompt for AI Agents |
||
|
|
||
| fn endpoint_reachable(endpoint: &reqwest::Url, timeout: Duration) -> bool { | ||
|
|
@@ -2085,7 +2087,7 @@ mod tests { | |
| "docs.example.com".into(), | ||
| String::new(), | ||
| ]; | ||
| let normalized = normalize_domains(domains); | ||
| let normalized = normalize_domains(domains).unwrap(); | ||
| assert_eq!(normalized, vec!["example.com", "docs.example.com"]); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,7 @@ impl BrowserOpenTool { | |
| pub fn new(security: Arc<SecurityPolicy>, allowed_domains: Vec<String>) -> Self { | ||
| Self { | ||
| security, | ||
| allowed_domains: normalize_allowed_domains(allowed_domains), | ||
| allowed_domains: normalize_allowed_domains(allowed_domains).expect("Invalid allowlist entries"), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid Invalid allowlist config should return a typed error, not abort process initialization. 💡 Suggested fix impl BrowserOpenTool {
- pub fn new(security: Arc<SecurityPolicy>, allowed_domains: Vec<String>) -> Self {
- Self {
+ pub fn new(security: Arc<SecurityPolicy>, allowed_domains: Vec<String>) -> anyhow::Result<Self> {
+ let allowed_domains = normalize_allowed_domains(allowed_domains)
+ .context("Invalid browser.allowed_domains configuration")?;
+ Ok(Self {
security,
- allowed_domains: normalize_allowed_domains(allowed_domains).expect("Invalid allowlist entries"),
- }
+ allowed_domains,
+ })
}
}🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
|
|
@@ -258,7 +258,7 @@ mod tests { | |
| "EXAMPLE.COM".into(), | ||
| "https://example.com/".into(), | ||
| ]); | ||
| assert_eq!(got, vec!["example.com".to_string()]); | ||
| assert_eq!(got.unwrap(), vec!["example.com".to_string()]); | ||
| } | ||
|
|
||
| #[test] | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -70,7 +70,7 @@ impl HttpRequestTool { | |||||||||||||||||||||||||||||||||||||
| ) -> Self { | ||||||||||||||||||||||||||||||||||||||
| Self { | ||||||||||||||||||||||||||||||||||||||
| security, | ||||||||||||||||||||||||||||||||||||||
| allowed_domains: normalize_allowed_domains(allowed_domains), | ||||||||||||||||||||||||||||||||||||||
| allowed_domains: normalize_allowed_domains(allowed_domains).expect("Invalid allowlist entries"), | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not panic on invalid allowlist in Use error propagation from constructor instead of 💡 Suggested fix impl HttpRequestTool {
- pub fn new(
+ pub fn new(
security: Arc<SecurityPolicy>,
allowed_domains: Vec<String>,
max_response_size: usize,
timeout_secs: u64,
- ) -> Self {
- Self {
+ ) -> anyhow::Result<Self> {
+ let allowed_domains = normalize_allowed_domains(allowed_domains)
+ .context("Invalid http_request.allowed_domains configuration")?;
+ Ok(Self {
security,
- allowed_domains: normalize_allowed_domains(allowed_domains).expect("Invalid allowlist entries"),
+ allowed_domains,
max_response_size,
timeout_secs,
- }
+ })
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| max_response_size, | ||||||||||||||||||||||||||||||||||||||
| timeout_secs, | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -472,7 +472,7 @@ mod tests { | |||||||||||||||||||||||||||||||||||||
| "EXAMPLE.COM".into(), | ||||||||||||||||||||||||||||||||||||||
| "https://example.com/".into(), | ||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||
| assert_eq!(got, vec!["example.com".to_string()]); | ||||||||||||||||||||||||||||||||||||||
| assert_eq!(got.unwrap(), vec!["example.com".to_string()]); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,64 @@ | ||||||||||||||||
| use corvus::identity::load_aieos_identity; | ||||||||||||||||
| use corvus::config::IdentityConfig; | ||||||||||||||||
| use tempfile::tempdir; | ||||||||||||||||
| use std::fs; | ||||||||||||||||
| use std::os::unix::fs::symlink; | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find the file
echo "Looking for identity_security.rs..."
find . -name "identity_security.rs" -type f 2>/dev/null | head -20Repository: dallay/corvus Length of output: 143 🏁 Script executed: #!/bin/bash
# Check if the file exists and read its content
if [ -f "clients/agent-runtime/tests/identity_security.rs" ]; then
echo "File found. Reading content..."
wc -l "clients/agent-runtime/tests/identity_security.rs"
echo "---"
head -60 "clients/agent-runtime/tests/identity_security.rs"
else
echo "File not found at expected location"
fiRepository: dallay/corvus Length of output: 2096 🏁 Script executed: #!/bin/bash
# Check CI workflows for non-Unix targets
echo "Checking CI/workflows for non-Unix targets/runners..."
if [ -d ".github/workflows" ]; then
find .github/workflows -type f \( -name "*.yml" -o -name "*.yaml" \) | while read f; do
echo "=== $f ==="
cat "$f" | grep -E "(windows|macos|ubuntu|target|runs-on|os:)" || echo " (no matches found)"
done
else
echo "No .github/workflows directory found"
fiRepository: dallay/corvus Length of output: 3921 🏁 Script executed: #!/bin/bash
# Check Cargo.toml for target specifications
echo "Checking Cargo.toml files for target specifications..."
find clients/agent-runtime -name "Cargo.toml" -type f | while read f; do
echo "=== $f ==="
head -50 "$f" | grep -E "(target|platform)" || echo " (no explicit targets)"
doneRepository: dallay/corvus Length of output: 659 Add platform guards to Unix-only symlink imports to prevent Windows build failures.
🔧 Proposed fix use corvus::identity::load_aieos_identity;
use corvus::config::IdentityConfig;
use tempfile::tempdir;
use std::fs;
-use std::os::unix::fs::symlink;
+#[cfg(unix)]
+use std::os::unix::fs::symlink;
@@
#[test]
+#[cfg(unix)]
fn test_load_aieos_identity_traversal_symlink() {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| #[test] | ||||||||||||||||
| fn test_load_aieos_identity_within_workspace() { | ||||||||||||||||
| let workspace = tempdir().unwrap(); | ||||||||||||||||
| let identity_file = workspace.path().join("identity.json"); | ||||||||||||||||
| fs::write(&identity_file, "{\"identity\":{\"names\":{\"first\":\"Test\"}}}").unwrap(); | ||||||||||||||||
|
|
||||||||||||||||
| let config = IdentityConfig { | ||||||||||||||||
| format: "aieos".into(), | ||||||||||||||||
| aieos_path: Some("identity.json".into()), | ||||||||||||||||
| aieos_inline: None, | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| let result = load_aieos_identity(&config, workspace.path()).unwrap(); | ||||||||||||||||
| assert!(result.is_some()); | ||||||||||||||||
| assert_eq!(result.unwrap().identity.unwrap().names.unwrap().first.unwrap(), "Test"); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| #[test] | ||||||||||||||||
| fn test_load_aieos_identity_traversal_dots() { | ||||||||||||||||
| let workspace = tempdir().unwrap(); | ||||||||||||||||
| let outside = tempdir().unwrap(); | ||||||||||||||||
| let identity_file = outside.path().join("identity.json"); | ||||||||||||||||
| fs::write(&identity_file, "{}").unwrap(); | ||||||||||||||||
|
|
||||||||||||||||
| let relative_path = format!("../{}/identity.json", | ||||||||||||||||
| outside.path().file_name().unwrap().to_str().unwrap()); | ||||||||||||||||
|
|
||||||||||||||||
| let config = IdentityConfig { | ||||||||||||||||
| format: "aieos".into(), | ||||||||||||||||
| aieos_path: Some(relative_path), | ||||||||||||||||
| aieos_inline: None, | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| let result = load_aieos_identity(&config, workspace.path()); | ||||||||||||||||
| assert!(result.is_err()); | ||||||||||||||||
| assert!(result.unwrap_err().to_string().contains("outside the workspace")); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| #[test] | ||||||||||||||||
| fn test_load_aieos_identity_traversal_symlink() { | ||||||||||||||||
| let workspace = tempdir().unwrap(); | ||||||||||||||||
| let outside = tempdir().unwrap(); | ||||||||||||||||
| let identity_file = outside.path().join("identity.json"); | ||||||||||||||||
| fs::write(&identity_file, "{}").unwrap(); | ||||||||||||||||
|
|
||||||||||||||||
| let link_path = workspace.path().join("malicious_link.json"); | ||||||||||||||||
| symlink(&identity_file, &link_path).unwrap(); | ||||||||||||||||
|
|
||||||||||||||||
| let config = IdentityConfig { | ||||||||||||||||
| format: "aieos".into(), | ||||||||||||||||
| aieos_path: Some("malicious_link.json".into()), | ||||||||||||||||
| aieos_inline: None, | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| let result = load_aieos_identity(&config, workspace.path()); | ||||||||||||||||
| assert!(result.is_err()); | ||||||||||||||||
| assert!(result.unwrap_err().to_string().contains("outside the workspace")); | ||||||||||||||||
| } | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
Repository: dallay/corvus
Length of output: 3618
🏁 Script executed:
Repository: dallay/corvus
Length of output: 496
🏁 Script executed:
Repository: dallay/corvus
Length of output: 507
Consider using POSIX character classes for portability, though current patterns work on this runner.
While
\sand\bfunction correctly in the grep patterns on standard environments, they are not guaranteed to work consistently across all POSIX ERE implementations. The patterns on lines 7, 11, 20, and 32 can be hardened by replacing\swith[[:space:]]and adding explicit suffix matching for type parameters and inheritance declarations.The escaping on line 18 currently handles only
.; expanding it to[][(){}.^$*+?|\\-]guards against future edge cases with special characters in identifiers, though typical Kotlin names rarely contain these.Suggested improvements
Verify each finding against the current code and only fix it if needed.
In @.agents/skills/kotlin-multiplatform/scripts/validate-kmp-structure.sh around
lines 66 - 70, Update the regexes in validate-kmp-structure.sh to use POSIX
character classes and stricter suffix matches: replace instances of "\s" and
"\b" in the grep patterns used to compute expect_files_count and to extract
expects (the grep line and the later grep in the "expects" assignment) with
"[[:space:]]" and ensure the following token match anchors allow type parameter
or inheritance suffixes (e.g., match "<", ":", or end-of-line) rather than
relying on "\b"; also expand the identifier-sanitizing sed/awk step that strips
trailing punctuation to account for additional special characters (use a
bracketed class like "[][(){}.^$*+?|\-]" when removing trailing tokens) so
functions/variables that reference expect_files_count and the extraction block
produce portable, robust matches across POSIX systems.