-
Notifications
You must be signed in to change notification settings - Fork 5
fix: follow absolute symlink directories from host mount #1655
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -241,7 +241,9 @@ impl HostScanner { | |
| self.metrics.scan_inc(ScanLabels::FileScanned); | ||
| } else if metadata.is_symlink() { | ||
| self.metrics.scan_inc(ScanLabels::SymlinkScanned); | ||
| self.scan_symlink(&path); | ||
| if let Err(e) = self.scan_symlink(&path) { | ||
| warn!("Failed to scan symlink {}: {e:#}", path.display()); | ||
| } | ||
| } else if metadata.is_dir() { | ||
| self.metrics.scan_inc(ScanLabels::DirectoryScanned); | ||
| } else { | ||
|
|
@@ -255,34 +257,64 @@ impl HostScanner { | |
| Ok(()) | ||
| } | ||
|
|
||
| fn scan_symlink(&self, path: &Path) { | ||
| let target = match path.read_link() { | ||
| Ok(p) => { | ||
| if p.has_root() { | ||
| &host_info::prepend_host_mount(&p) | ||
| } else { | ||
| path | ||
| } | ||
| } | ||
| Err(e) => { | ||
| warn!("Failed to read symlink path: {e}"); | ||
| return; | ||
| } | ||
| fn scan_symlink(&self, path: &Path) -> anyhow::Result<()> { | ||
| let link_target = path | ||
| .read_link() | ||
| .with_context(|| format!("failed to read symlink {}", path.display()))?; | ||
| let target = if link_target.has_root() { | ||
| // FACT scans the host below FACT_HOST_MOUNT. Absolute links are | ||
| // absolute in the host filesystem, not in FACT's container. | ||
| host_info::prepend_host_mount(&link_target) | ||
| } else { | ||
| path.parent().unwrap_or(Path::new("/")).join(link_target) | ||
| }; | ||
| let metadata = target | ||
| .metadata() | ||
| .with_context(|| format!("failed to read symlink target {}", target.display()))?; | ||
| self.update_entry(path, &metadata) | ||
| .with_context(|| format!("failed to update symlink entry for {}", path.display()))?; | ||
|
|
||
| match target.metadata() { | ||
| Ok(metadata) => { | ||
| if let Err(e) = self.update_entry(path, &metadata) { | ||
| warn!("Failed to update symlink entry for {}: {e}", path.display()); | ||
| if !metadata.is_dir() { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| // glob expands an absolute symlink relative to FACT's container root. | ||
| // Expand the translated target instead, but retain the configured | ||
| // symlink path as the inode-map alias used for event enrichment. | ||
| let recursive_target = target.join("**/*"); | ||
| let glob_str = recursive_target | ||
| .to_str() | ||
| .context("invalid recursive symlink target path")?; | ||
| for entry in glob::glob(glob_str)? { | ||
|
Comment on lines
+284
to
+288
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. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🌐 Web query:
💡 Result: The Rust glob crate does not natively support following symbolic links when performing recursive searches [1]. Consequently, it does not have built-in mechanisms to detect or handle infinite recursion caused by symlink cycles [1]. If you require functionality to follow symbolic links or manage cycles, you may need to use an alternative crate such as globwalk, which provides a GlobWalkerBuilder with a follow_links method to enable or disable link following [2]. Regarding Pattern::escape, this function is provided by the Pattern struct in the glob crate [3]. Its purpose is to escape metacharacters within a given string by enclosing them in brackets [3][4]. The resulting string, when compiled into a Pattern, will match the exact input string and no other patterns [3][4]. Top results: [1] rust-lang/glob#62 [2] https://docs.rs/globwalk/latest/globwalk/struct.GlobWalkerBuilder.html [3] https://docs.rs/glob/latest/glob/struct.Pattern.html Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- changed hunk and surrounding code ---'
sed -n '240,315p' fact/src/host_scanner.rs
printf '%s\n' '--- glob dependency declarations ---'
rg -n -C 3 '(^|[^[:alnum:]_])glob([^[:alnum:]_]|$)|glob\s*=' --glob 'Cargo.toml' --glob 'Cargo.lock' .
printf '%s\n' '--- relevant scanner symbols and callers ---'
rg -n -C 3 'recursive_target|host_scanner|read_dir|symlink' fact/src/host_scanner.rs fact/src --glob '*.rs'Repository: stackrox/fact Length of output: 30564 🌐 Web query:
💡 Result: In the Rust glob crate (version 0.3.4), the handling of recursive patterns and symlinks, along with the functionality of Pattern::escape, are as follows: Recursive Patterns and Symlinks The glob crate supports recursive directory matching using the syntax (e.g., /media//*.jpg) [1][2]. Regarding symlinks, the crate's behavior is implemented to determine whether a path is a directory by checking file metadata [3]. When encountering a symlink, the crate generally respects standard filesystem metadata operations [3]. Note that specific handling or control over whether symlinks are followed during recursive traversal is not explicitly exposed as a high-level configuration flag in the glob crate's API [4][3]. Users needing specific symlink traversal behavior often rely on the underlying behavior of Rust's standard library fs::metadata calls [3]. Pattern::escape The Pattern::escape function is a static method provided by the Pattern struct in the glob crate [4][5]. Its purpose is to escape meta-characters within a string so that the resulting string can be compiled into a Pattern that matches the input string literally and nothing else [5]. It achieves this by surrounding identified meta-characters with brackets [5]. For example, if you have a string that contains characters like * or?, passing it to Pattern::escape will return a string that, when used as a pattern, will match that exact filename regardless of the meta-characters it originally contained [5]. Technical Summary - Recursive matching: Supported via [1][2]. - Symlinks: Handled via standard metadata inspection; no explicit "follow_links" toggle is present in the public API [3]. - Pattern::escape: Used to treat characters as literals by wrapping them in brackets [5]. Citations:
🌐 Web query:
💡 Result: In
The search result currently resolves primarily to the equivalent Citations: Replace 🤖 Prompt for AI Agents |
||
| let target_path = match entry { | ||
| Ok(path) => path, | ||
| Err(e) => { | ||
| debug!("Glob expansion failed: {e:?}"); | ||
| self.metrics.scan_inc(ScanLabels::GlobFailed); | ||
| continue; | ||
| } | ||
| } | ||
| Err(e) => { | ||
| warn!( | ||
| "Failed to read metadata for symlink target {}: {e}", | ||
| }; | ||
| let suffix = target_path.strip_prefix(&target).with_context(|| { | ||
| format!( | ||
| "symlink target {} escaped recursive root {}", | ||
| target_path.display(), | ||
| target.display() | ||
| ); | ||
| } | ||
| ) | ||
| })?; | ||
| let metadata = match target_path.symlink_metadata() { | ||
| Ok(metadata) => metadata, | ||
| Err(e) if e.kind() == io::ErrorKind::NotFound => continue, | ||
| Err(e) => { | ||
| warn!("Failed to get metadata for {}: {e}", target_path.display()); | ||
| continue; | ||
| } | ||
| }; | ||
| self.update_entry(&path.join(suffix), &metadata).with_context(|| { | ||
| format!("failed to update symlink descendant {}", target_path.display()) | ||
| })?; | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| /// Do a partial scan of any pattern that matches the provided path | ||
|
|
||
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.
This will just be a source of false positives because it discards the original glob pattern that lead us to the symlink in the first place. It also doesn't work if a symlink is somewhere in the middle of a path pattern, because the glob algorithm will already have ignored it.
The correct solution is to implement our own glob expansion algorithm, but that is not something I would trust on an agent to do properly.