diff --git a/CHANGELOG.md b/CHANGELOG.md index 93299071..ca27cddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ possible include a PR number for easier tracking. * ROX-34502: reload mTLS certificates on each gRPC connection attempt (#788) * chore: add formatting and linting to integration test code (#783, #784) * feat: add code coverage with cargo-llvm-cov and Codecov upload (#745) +* ROX-30296: adds ACL tracking via inode_set_acl LSM hook (#878) ## 0.3.0 diff --git a/fact-ebpf/src/bpf/checks.c b/fact-ebpf/src/bpf/checks.c index 1b3623a2..3e8bb177 100644 --- a/fact-ebpf/src/bpf/checks.c +++ b/fact-ebpf/src/bpf/checks.c @@ -13,3 +13,9 @@ int BPF_PROG(check_path_unlink_supports_bpf_d_path, struct path* dir, struct den bpf_printk("dir: %s", p->path); return 0; } + +SEC("lsm/inode_set_acl") +int BPF_PROG(check_inode_set_acl, struct mnt_idmap* idmap, struct dentry* dentry, const char* acl_name, + struct posix_acl* kacl) { + return 0; +} diff --git a/fact-ebpf/src/bpf/events.h b/fact-ebpf/src/bpf/events.h index efedf86e..7dc4b766 100644 --- a/fact-ebpf/src/bpf/events.h +++ b/fact-ebpf/src/bpf/events.h @@ -160,3 +160,45 @@ __always_inline static void submit_xattr_event(struct submit_event_args_t* args, __submit_event(args, false); } + +__always_inline static void submit_acl_event(struct submit_event_args_t* args, + const char* acl_name, + struct posix_acl* kacl) { + if (!reserve_event(args)) { + return; + } + + args->event->type = FILE_ACTIVITY_ACL_SET; + + // Determine ACL type from the xattr name. + // "system.posix_acl_access" vs "system.posix_acl_default" + char name_buf[32] = {0}; + long name_len = bpf_probe_read_kernel_str(name_buf, sizeof(name_buf), acl_name); + if (name_len == 25 && __builtin_memcmp(name_buf, "system.posix_acl_default", 24) == 0) { + args->event->acl.acl_type = FACT_ACL_TYPE_DEFAULT; + } else { + args->event->acl.acl_type = FACT_ACL_TYPE_ACCESS; + } + + if (kacl == NULL) { + args->event->acl.count = 0; + } else { + unsigned int count = 0; + bpf_probe_read_kernel(&count, sizeof(count), &kacl->a_count); + if (count > FACT_MAX_ACL_ENTRIES) { + count = FACT_MAX_ACL_ENTRIES; + } + args->event->acl.count = count; + + for (unsigned int i = 0; i < FACT_MAX_ACL_ENTRIES && i < count; i++) { + struct posix_acl_entry entry = {0}; + bpf_probe_read_kernel(&entry, sizeof(entry), &kacl->a_entries[i]); + args->event->acl.entries[i].e_tag = entry.e_tag; + args->event->acl.entries[i].e_perm = entry.e_perm; + args->event->acl.entries[i].e_id = entry.e_uid.val; + } + } + + // inode_set_acl does not support bpf_d_path (no struct path available) + __submit_event(args, false); +} diff --git a/fact-ebpf/src/bpf/main.c b/fact-ebpf/src/bpf/main.c index 4eacdd79..b880a573 100644 --- a/fact-ebpf/src/bpf/main.c +++ b/fact-ebpf/src/bpf/main.c @@ -431,6 +431,31 @@ int BPF_PROG(trace_inode_removexattr, struct mnt_idmap* idmap, struct dentry* de return handle_xattr(&m->inode_removexattr, dentry, name, FILE_ACTIVITY_REMOVEXATTR); } +SEC("lsm/inode_set_acl") +int BPF_PROG(trace_inode_set_acl, struct mnt_idmap* idmap, struct dentry* dentry, + const char* acl_name, struct posix_acl* kacl) { + struct metrics_t* m = get_metrics(); + if (m == NULL) { + return 0; + } + struct submit_event_args_t args = {.metrics = &m->inode_set_acl}; + + args.metrics->total++; + + args.inode = inode_to_key(dentry->d_inode); + args.parent_inode = inode_to_key(BPF_CORE_READ(dentry, d_parent, d_inode)); + + args.monitored = inode_is_monitored(inode_get(&args.inode), inode_get(&args.parent_inode)); + + if (args.monitored == NOT_MONITORED) { + args.metrics->ignored++; + return 0; + } + + submit_acl_event(&args, acl_name, kacl); + return 0; +} + SEC("lsm/path_rmdir") int BPF_PROG(trace_path_rmdir, struct path* dir, struct dentry* dentry) { struct metrics_t* m = get_metrics(); diff --git a/fact-ebpf/src/bpf/types.h b/fact-ebpf/src/bpf/types.h index 3d3ec49e..11b7277d 100644 --- a/fact-ebpf/src/bpf/types.h +++ b/fact-ebpf/src/bpf/types.h @@ -58,6 +58,18 @@ typedef enum monitored_t { // For the time being we just keep a char. typedef char inode_value_t; +#define FACT_MAX_ACL_ENTRIES 32 + +// ACL type constants matching the xattr names +#define FACT_ACL_TYPE_ACCESS 0 +#define FACT_ACL_TYPE_DEFAULT 1 + +struct acl_entry_t { + short e_tag; + unsigned short e_perm; + unsigned int e_id; +}; + typedef enum file_activity_type_t { FILE_ACTIVITY_INIT = -1, FILE_ACTIVITY_OPEN = 0, @@ -70,6 +82,7 @@ typedef enum file_activity_type_t { DIR_ACTIVITY_UNLINK, FILE_ACTIVITY_SETXATTR, FILE_ACTIVITY_REMOVEXATTR, + FILE_ACTIVITY_ACL_SET, } file_activity_type_t; struct event_t { @@ -99,6 +112,11 @@ struct event_t { struct { char name[XATTR_NAME_MAX_LEN]; } xattr; + struct { + unsigned int count; + unsigned int acl_type; + struct acl_entry_t entries[FACT_MAX_ACL_ENTRIES]; + } acl; }; }; @@ -143,4 +161,5 @@ struct metrics_t { struct metrics_by_hook_t path_rmdir; struct metrics_by_hook_t inode_setxattr; struct metrics_by_hook_t inode_removexattr; + struct metrics_by_hook_t inode_set_acl; }; diff --git a/fact-ebpf/src/lib.rs b/fact-ebpf/src/lib.rs index 8b66d92b..4117e983 100644 --- a/fact-ebpf/src/lib.rs +++ b/fact-ebpf/src/lib.rs @@ -155,6 +155,9 @@ impl_metrics_t!( path_mkdir, path_rmdir, d_instantiate, + inode_setxattr, + inode_removexattr, + inode_set_acl, ); unsafe impl Pod for metrics_t {} diff --git a/fact/src/bpf/checks.rs b/fact/src/bpf/checks.rs index 1686fdfc..d0c88428 100644 --- a/fact/src/bpf/checks.rs +++ b/fact/src/bpf/checks.rs @@ -4,6 +4,7 @@ use log::debug; pub(super) struct Checks { pub(super) path_hooks_support_bpf_d_path: bool, + pub(super) supports_inode_set_acl: bool, } impl Checks { @@ -12,15 +13,31 @@ impl Checks { .load(fact_ebpf::CHECKS_OBJ) .context("Failed to load checks.o")?; - let prog = obj - .program_mut("check_path_unlink_supports_bpf_d_path") - .context("Failed to find 'check_path_unlink_supports_bpf_d_path' program")?; - let prog: &mut Lsm = prog.try_into()?; - let path_hooks_support_bpf_d_path = prog.load("path_unlink", btf).is_ok(); - debug!("path_unlink_supports_bpf_d_path: {path_hooks_support_bpf_d_path}"); + let path_hooks_support_bpf_d_path = Self::probe_hook( + &mut obj, + "check_path_unlink_supports_bpf_d_path", + "path_unlink", + btf, + ); + debug!("path_hooks_support_bpf_d_path: {path_hooks_support_bpf_d_path}"); + + let supports_inode_set_acl = + Self::probe_hook(&mut obj, "check_inode_set_acl", "inode_set_acl", btf); + debug!("supports_inode_set_acl: {supports_inode_set_acl}"); Ok(Checks { path_hooks_support_bpf_d_path, + supports_inode_set_acl, }) } + + fn probe_hook(obj: &mut aya::Ebpf, prog_name: &str, hook: &str, btf: &Btf) -> bool { + let Some(prog) = obj.program_mut(prog_name) else { + return false; + }; + let Ok(prog): Result<&mut Lsm, _> = prog.try_into() else { + return false; + }; + prog.load(hook, btf).is_ok() + } } diff --git a/fact/src/bpf/mod.rs b/fact/src/bpf/mod.rs index 24d091c6..898c543d 100644 --- a/fact/src/bpf/mod.rs +++ b/fact/src/bpf/mod.rs @@ -26,6 +26,7 @@ const RINGBUFFER_NAME: &str = "rb"; pub struct Bpf { obj: Ebpf, + checks: Checks, tx: mpsc::Sender, @@ -64,6 +65,7 @@ impl Bpf { let paths = Vec::new(); let mut bpf = Bpf { obj, + checks, tx, paths, paths_config, @@ -178,28 +180,38 @@ impl Bpf { let Some(hook) = name.strip_prefix("trace_") else { bail!("Invalid hook name: {name}"); }; + + // Skip hooks that the kernel doesn't support + if hook == "inode_set_acl" && !self.checks.supports_inode_set_acl { + info!("Skipping {hook}: not supported on this kernel"); + continue; + } + match prog { Program::Lsm(prog) => prog.load(hook, btf)?, u => unimplemented!("{u:?}"), - } + }; } Ok(()) } - /// Attaches all BPF programs. If any attach fails, all previously - /// attached programs are automatically detached via drop. + /// Attaches all loaded BPF programs. Programs that were not loaded + /// (e.g. optional hooks on unsupported kernels) are skipped. + /// If any attach fails, all previously attached programs are + /// automatically detached via drop. fn attach_progs(&mut self) -> anyhow::Result<()> { - self.links = self - .obj - .programs_mut() - .map(|(_, prog)| match prog { + for (_, prog) in self.obj.programs_mut() { + match prog { Program::Lsm(prog) => { + if prog.fd().is_err() { + continue; + } let link_id = prog.attach()?; - prog.take_link(link_id) + self.links.push(prog.take_link(link_id)?); } u => unimplemented!("{u:?}"), - }) - .collect::>()?; + } + } Ok(()) } diff --git a/fact/src/event/mod.rs b/fact/src/event/mod.rs index a91c5f00..93ccb835 100644 --- a/fact/src/event/mod.rs +++ b/fact/src/event/mod.rs @@ -170,6 +170,7 @@ impl Event { FileData::Rename(data) => &data.new.inode, FileData::SetXattr(data) => &data.inner.inode, FileData::RemoveXattr(data) => &data.inner.inode, + FileData::AclSet(data) => &data.inner.inode, } } @@ -186,6 +187,7 @@ impl Event { FileData::Rename(data) => &data.new.parent_inode, FileData::SetXattr(data) => &data.inner.parent_inode, FileData::RemoveXattr(data) => &data.inner.parent_inode, + FileData::AclSet(data) => &data.inner.parent_inode, } } @@ -211,6 +213,7 @@ impl Event { FileData::Rename(data) => &data.new.filename, FileData::SetXattr(data) => &data.inner.filename, FileData::RemoveXattr(data) => &data.inner.filename, + FileData::AclSet(data) => &data.inner.filename, } } @@ -233,6 +236,7 @@ impl Event { FileData::Rename(data) => &data.new.host_file, FileData::SetXattr(data) => &data.inner.host_file, FileData::RemoveXattr(data) => &data.inner.host_file, + FileData::AclSet(data) => &data.inner.host_file, } } @@ -259,6 +263,7 @@ impl Event { FileData::Rename(data) => data.new.host_file = host_path, FileData::SetXattr(data) => data.inner.host_file = host_path, FileData::RemoveXattr(data) => data.inner.host_file = host_path, + FileData::AclSet(data) => data.inner.host_file = host_path, } } @@ -282,6 +287,7 @@ impl Event { FileData::Rename(data) => data.new.monitored, FileData::SetXattr(data) => data.inner.monitored, FileData::RemoveXattr(data) => data.inner.monitored, + FileData::AclSet(data) => data.inner.monitored, } } @@ -376,6 +382,7 @@ pub enum FileData { Rename(RenameFileData), SetXattr(XattrFileData), RemoveXattr(XattrFileData), + AclSet(AclSetFileData), } impl FileData { @@ -439,6 +446,35 @@ impl FileData { )?; FileData::RemoveXattr(XattrFileData { inner, xattr_name }) } + file_activity_type_t::FILE_ACTIVITY_ACL_SET => { + let acl = unsafe { &extra_data.acl }; + let acl_type = if acl.acl_type == fact_ebpf::FACT_ACL_TYPE_DEFAULT { + AclType::Default + } else { + AclType::Access + }; + let count = acl.count.min(fact_ebpf::FACT_MAX_ACL_ENTRIES) as usize; + let mut entries = Vec::with_capacity(count); + for i in 0..count { + let entry = &acl.entries[i]; + let tag = AclTag::from_kernel(entry.e_tag); + let id = if tag.has_qualifier() { + Some(entry.e_id) + } else { + None + }; + entries.push(AclEntry { + tag, + perm: entry.e_perm, + id, + }); + } + FileData::AclSet(AclSetFileData { + inner, + acl_type, + entries, + }) + } invalid => unreachable!("Invalid event type: {invalid:?}"), }; @@ -490,6 +526,10 @@ impl From for fact_api::file_activity::File { let f_act = fact_api::FileRename::from(event); fact_api::file_activity::File::Rename(f_act) } + FileData::AclSet(event) => { + let f_act = fact_api::FileAclChange::from(event); + fact_api::file_activity::File::Acl(f_act) + } } } } @@ -507,6 +547,11 @@ impl PartialEq for FileData { (FileData::Rename(this), FileData::Rename(other)) => this == other, (FileData::SetXattr(this), FileData::SetXattr(other)) => this == other, (FileData::RemoveXattr(this), FileData::RemoveXattr(other)) => this == other, + (FileData::AclSet(this), FileData::AclSet(other)) => { + this.inner == other.inner + && this.acl_type == other.acl_type + && this.entries == other.entries + } _ => false, } } @@ -619,6 +664,56 @@ pub struct RenameFileData { old: BaseFileData, } +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +pub enum AclTag { + UserObj, + User, + GroupObj, + Group, + Mask, + Other, + Unknown(i16), +} + +impl AclTag { + fn from_kernel(tag: i16) -> Self { + match tag { + 0x01 => AclTag::UserObj, + 0x02 => AclTag::User, + 0x04 => AclTag::GroupObj, + 0x08 => AclTag::Group, + 0x10 => AclTag::Mask, + 0x20 => AclTag::Other, + other => AclTag::Unknown(other), + } + } + + /// Whether this tag type carries a meaningful uid/gid. + fn has_qualifier(&self) -> bool { + matches!(self, AclTag::User | AclTag::Group) + } +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +pub struct AclEntry { + pub tag: AclTag, + pub perm: u16, + pub id: Option, +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +pub enum AclType { + Access, + Default, +} + +#[derive(Debug, Clone, Serialize)] +pub struct AclSetFileData { + inner: BaseFileData, + pub acl_type: AclType, + pub entries: Vec, +} + impl From for fact_api::FileRename { fn from(RenameFileData { new, old }: RenameFileData) -> Self { let new = fact_api::FileActivityBase::from(new); @@ -630,6 +725,46 @@ impl From for fact_api::FileRename { } } +impl From for i32 { + fn from(tag: AclTag) -> Self { + match tag { + AclTag::UserObj => fact_api::AclTag::UserObj as i32, + AclTag::User => fact_api::AclTag::User as i32, + AclTag::GroupObj => fact_api::AclTag::GroupObj as i32, + AclTag::Group => fact_api::AclTag::Group as i32, + AclTag::Mask => fact_api::AclTag::Mask as i32, + AclTag::Other => fact_api::AclTag::Other as i32, + AclTag::Unknown(_) => fact_api::AclTag::Unspecified as i32, + } + } +} + +impl From for fact_api::FileAclChange { + fn from(value: AclSetFileData) -> Self { + let activity = fact_api::FileActivityBase::from(value.inner); + let acl_type = match value.acl_type { + AclType::Access => fact_api::AclType::Access as i32, + AclType::Default => fact_api::AclType::Default as i32, + }; + let entries = value + .entries + .into_iter() + .map(|e| fact_api::AclEntry { + tag: i32::from(e.tag), + perm: e.perm as u32, + // ACL_UNDEFINED_ID (0xFFFFFFFF) for entries that don't + // carry a uid/gid (USER_OBJ, GROUP_OBJ, MASK, OTHER). + id: e.id.unwrap_or(0xFFFFFFFF), + }) + .collect(); + fact_api::FileAclChange { + activity: Some(activity), + acl_type, + entries, + } + } +} + #[cfg(test)] impl PartialEq for RenameFileData { fn eq(&self, other: &Self) -> bool { diff --git a/fact/src/metrics/kernel_metrics.rs b/fact/src/metrics/kernel_metrics.rs index 11fb5f65..d1e77bfb 100644 --- a/fact/src/metrics/kernel_metrics.rs +++ b/fact/src/metrics/kernel_metrics.rs @@ -72,4 +72,5 @@ define_kernel_metrics!( d_instantiate, inode_setxattr, inode_removexattr, + inode_set_acl, ); diff --git a/tests/event.py b/tests/event.py index 2993b396..05d5dc3e 100644 --- a/tests/event.py +++ b/tests/event.py @@ -48,6 +48,20 @@ class EventType(Enum): RENAME = 6 XATTR_SET = 7 XATTR_REMOVE = 8 + ACL = 9 + + +# POSIX ACL type values matching the AclType proto enum. +ACL_TYPE_ACCESS = 1 +ACL_TYPE_DEFAULT = 2 + +# POSIX ACL tag values matching the AclTag proto enum. +ACL_TAG_USER_OBJ = 1 +ACL_TAG_USER = 2 +ACL_TAG_GROUP_OBJ = 3 +ACL_TAG_GROUP = 4 +ACL_TAG_MASK = 5 +ACL_TAG_OTHER = 6 class Process: @@ -236,6 +250,8 @@ def __init__( old_file: str | Pattern[str] | None = None, old_host_path: str | Pattern[str] | None = None, xattr_name: str | None = None, + acl_type: int | None = None, + acl_entries: list[dict] | None = None, ): self._type: EventType = event_type self._process: Process = process @@ -247,6 +263,8 @@ def __init__( self._old_file: str | Pattern[str] | None = old_file self._old_host_path: str | Pattern[str] | None = old_host_path self._xattr_name: str | None = xattr_name + self._acl_type: int | None = acl_type + self._acl_entries: list[dict] | None = acl_entries @property def event_type(self) -> EventType: @@ -288,6 +306,14 @@ def old_host_path(self) -> str | Pattern[str] | None: def xattr_name(self) -> str | None: return self._xattr_name + @property + def acl_type(self) -> int | None: + return self._acl_type + + @property + def acl_entries(self) -> list[dict] | None: + return self._acl_entries + @classmethod def _diff_field(cls, diff: dict, name: str, expected: Any, actual: Any): if expected != actual: @@ -403,6 +429,28 @@ def diff(self, other: FileActivity) -> dict | None: self.xattr_name, event_field.xattr_name, ) + elif self.event_type == EventType.ACL: + Event._diff_field( + diff, + 'acl_type', + self.acl_type, + event_field.acl_type, + ) + if self.acl_entries is not None: + actual_entries = [ + { + 'tag': e.tag, + 'perm': e.perm, + 'id': e.id, + } + for e in event_field.entries + ] + Event._diff_field( + diff, + 'acl_entries', + self.acl_entries, + actual_entries, + ) return diff if diff else None @@ -429,6 +477,10 @@ def __str__(self) -> str: if self.event_type in (EventType.XATTR_SET, EventType.XATTR_REMOVE): s += f', xattr_name="{self.xattr_name}"' + if self.event_type == EventType.ACL: + s += f', acl_type={self.acl_type}' + s += f', acl_entries={self.acl_entries}' + s += ')' return s diff --git a/tests/server.py b/tests/server.py index 89835cd6..c2a69330 100644 --- a/tests/server.py +++ b/tests/server.py @@ -15,6 +15,15 @@ from event import Event +# Mapping from friendly skip names to protobuf oneof field names. +SKIP_EVENT_TYPES: dict[str, tuple[str, ...]] = { + 'xattr': ('xattr_set', 'xattr_remove'), + 'acl': ('acl',), +} + +DEFAULT_SKIP = ('xattr', 'acl') + + class FileActivityService(sfa_iservice_pb2_grpc.FileActivityServiceServicer): """ GRPC server for the File Activity Service. @@ -92,7 +101,7 @@ def _wait_events( self, events: list[Event], strict: bool, - skip_xattr: bool, + skip_oneof_names: frozenset[str], cancel: ThreadingEvent, ): while self.is_running() and not cancel.is_set(): @@ -103,10 +112,7 @@ def _wait_events( print(f'Got event: {msg}') - if skip_xattr and msg.WhichOneof('file') in ( - 'xattr_set', - 'xattr_remove', - ): + if msg.WhichOneof('file') in skip_oneof_names: continue # Check if msg matches the next expected event @@ -122,7 +128,7 @@ def wait_events( self, events: list[Event], strict: bool = True, - skip_xattr: bool = True, + skip: tuple[str, ...] = DEFAULT_SKIP, ): """ Continuously checks the server for incoming events until the @@ -131,17 +137,22 @@ def wait_events( Args: events (list['Event']): The events to search for. strict (bool): Fail if an unexpected event is detected. + skip: Event categories to silently ignore (e.g. 'xattr', + 'acl'). Pass an empty tuple to receive all events. Raises: TimeoutError: If the required events are not found in 5 seconds. """ + skip_oneof_names = frozenset( + name for key in skip for name in SKIP_EVENT_TYPES.get(key, (key,)) + ) print('Waiting for events:', *events, sep='\n') cancel = ThreadingEvent() fs = self.executor.submit( self._wait_events, events, strict, - skip_xattr, + skip_oneof_names, cancel, ) try: diff --git a/tests/test_acl.py b/tests/test_acl.py new file mode 100644 index 00000000..12e202f2 --- /dev/null +++ b/tests/test_acl.py @@ -0,0 +1,292 @@ +"""Tests for POSIX ACL change events. + +Uses os.setxattr to set ACLs directly via the POSIX ACL xattr wire +format, avoiding a dependency on the setfacl tool. +""" + +from __future__ import annotations + +import os +import struct + +import pytest + +from event import ( + ACL_TAG_GROUP, + ACL_TAG_GROUP_OBJ, + ACL_TAG_MASK, + ACL_TAG_OTHER, + ACL_TAG_USER, + ACL_TAG_USER_OBJ, + ACL_TYPE_ACCESS, + ACL_TYPE_DEFAULT, + Event, + EventType, + Process, +) +from server import FileActivityService +from utils import btf_has_symbol + +# POSIX ACL xattr wire format constants +_ACL_VERSION = 2 +_ACL_UNDEFINED_ID = 0xFFFFFFFF + +# Kernel ACL tag values (from include/uapi/linux/posix_acl.h) +_ACL_USER_OBJ = 0x01 +_ACL_USER = 0x02 +_ACL_GROUP_OBJ = 0x04 +_ACL_GROUP = 0x08 +_ACL_MASK = 0x10 +_ACL_OTHER = 0x20 + + +def _make_acl_xattr(entries: list[tuple[int, int, int]]) -> bytes: + """Build a POSIX ACL xattr value from a list of (tag, perm, id) tuples.""" + data = struct.pack(' bool: + """Check whether the running kernel's BTF contains a given symbol. + + Searches /sys/kernel/btf/vmlinux for the symbol name. Reads in + chunks to avoid loading the entire file into memory. + """ + needle = symbol.encode() + chunk_size = 64 * 1024 + try: + with open('/sys/kernel/btf/vmlinux', 'rb') as f: + # Read in chunks, keeping an overlap to catch matches + # that span chunk boundaries. + prev = b'' + while chunk := f.read(chunk_size): + if needle in prev + chunk: + return True + prev = chunk[-len(needle) :] + return False + except OSError: + return False + + def get_metric_value( fact_config: tuple[dict, str], metric_name: str, diff --git a/third_party/stackrox b/third_party/stackrox index 877de2af..2901ac80 160000 --- a/third_party/stackrox +++ b/third_party/stackrox @@ -1 +1 @@ -Subproject commit 877de2af9c2bbc4cb09b2003a0c2d6d88effe076 +Subproject commit 2901ac8090dce370fea1570555baa7618dd0bdcb