├── crates ├── rrg │ ├── LICENSE │ ├── src │ │ ├── lib.rs │ │ ├── ping.rs │ │ ├── blob.rs │ │ ├── action │ │ │ ├── list_mounts.rs │ │ │ ├── list_interfaces.rs │ │ │ ├── list_connections.rs │ │ │ ├── get_system_metadata.rs │ │ │ ├── get_winreg_value.rs │ │ │ ├── get_file_sha256.rs │ │ │ ├── list_utmp_users.rs │ │ │ └── query_wmi.rs │ │ ├── main.rs │ │ ├── bin │ │ │ └── rrg_oneshot.rs │ │ ├── startup.rs │ │ ├── session │ │ │ ├── fake.rs │ │ │ ├── fleetspeak.rs │ │ │ └── error.rs │ │ ├── args.rs │ │ ├── action.rs │ │ └── session.rs │ └── Cargo.toml ├── wmi │ ├── LICENSE │ ├── README.md │ ├── src │ │ ├── lib.rs │ │ └── windows │ │ │ └── bstr.rs │ └── Cargo.toml ├── ospect │ ├── LICENSE │ ├── README.md │ ├── src │ │ ├── lib.rs │ │ ├── os │ │ │ ├── linux.rs │ │ │ ├── macos.rs │ │ │ └── unix.rs │ │ ├── net │ │ │ └── unix.rs │ │ ├── proc.rs │ │ ├── proc │ │ │ ├── linux.rs │ │ │ ├── windows.rs │ │ │ └── macos.rs │ │ └── os.rs │ └── Cargo.toml ├── rrg-proto │ ├── LICENSE │ ├── .gitignore │ ├── Cargo.toml │ ├── src │ │ ├── convert.rs │ │ └── path │ │ │ └── mod.rs │ └── build.rs ├── winreg │ ├── LICENSE │ ├── README.md │ ├── src │ │ ├── lib.rs │ │ └── path.rs │ └── Cargo.toml ├── tsk-sys │ ├── wrapper.h │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── tsk │ ├── test_data │ └── smol.ntfs.gz │ └── Cargo.toml ├── .gitignore ├── .gitmodules ├── Cargo.toml ├── proto └── rrg │ ├── os.proto │ ├── action │ ├── list_mounts.proto │ ├── list_connections.proto │ ├── list_interfaces.proto │ ├── list_utmp_users.proto │ ├── get_winreg_value.proto │ ├── list_winreg_values.proto │ ├── get_tcp_response.proto │ ├── list_winreg_keys.proto │ ├── grep_file_contents.proto │ ├── get_file_sha256.proto │ ├── get_file_contents.proto │ ├── get_filesystem_timeline_tsk.proto │ ├── get_system_metadata.proto │ ├── query_wmi.proto │ ├── get_file_contents_kmx.proto │ ├── scan_memory_yara.proto │ ├── dump_process_memory.proto │ ├── get_filesystem_timeline.proto │ ├── execute_signed_command.proto │ └── get_file_metadata.proto │ ├── ping.proto │ ├── blob.proto │ ├── winreg.proto │ ├── startup.proto │ ├── net.proto │ └── fs.proto ├── LICENSE ├── CONTRIBUTING.md ├── .github └── workflows │ └── ci.yml ├── README.md └── docs └── creating-actions.md /crates/rrg/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /crates/wmi/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /crates/ospect/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /crates/rrg-proto/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /crates/winreg/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /crates/tsk-sys/wrapper.h: -------------------------------------------------------------------------------- 1 | #include 2 | -------------------------------------------------------------------------------- /crates/rrg-proto/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /crates/wmi/README.md: -------------------------------------------------------------------------------- 1 | wmi 2 | === 3 | 4 | A small library for running WQL queries. 5 | -------------------------------------------------------------------------------- /crates/ospect/README.md: -------------------------------------------------------------------------------- 1 | ospect 2 | ====== 3 | 4 | A small library for inspecting the operating system. 5 | -------------------------------------------------------------------------------- /crates/winreg/README.md: -------------------------------------------------------------------------------- 1 | winreg 2 | ====== 3 | 4 | A small library for querying the Windows registry. 5 | -------------------------------------------------------------------------------- /crates/tsk/test_data/smol.ntfs.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/rrg/HEAD/crates/tsk/test_data/smol.ntfs.gz -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/sleuthkit"] 2 | path = vendor/sleuthkit 3 | url = https://github.com/sleuthkit/sleuthkit 4 | -------------------------------------------------------------------------------- /crates/winreg/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod path; 2 | 3 | #[cfg(target_os = "windows")] 4 | mod windows; 5 | 6 | #[cfg(target_os = "windows")] 7 | pub use windows::*; 8 | -------------------------------------------------------------------------------- /crates/tsk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tsk" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies.tsk-sys] 8 | path = "../tsk-sys" 9 | 10 | [dev-dependencies] 11 | flate2 = "1.0.35" 12 | tempfile = "3.14.0" 13 | 14 | -------------------------------------------------------------------------------- /crates/ospect/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | mod libc; 7 | 8 | pub mod fs; 9 | pub mod net; 10 | pub mod os; 11 | pub mod proc; 12 | -------------------------------------------------------------------------------- /crates/wmi/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | #[cfg(target_os = "windows")] 7 | mod windows; 8 | 9 | #[cfg(target_os = "windows")] 10 | pub use windows::*; 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "./crates/ospect", 4 | "./crates/rrg", 5 | "./crates/rrg-proto", 6 | "./crates/tsk", 7 | "./crates/tsk-sys", 8 | "./crates/winreg", 9 | "./crates/wmi", 10 | ] 11 | resolver = "2" 12 | 13 | [workspace.package] 14 | version = "0.0.7" 15 | authors = ["Łukasz Hanuszczak "] 16 | edition = "2024" 17 | -------------------------------------------------------------------------------- /crates/winreg/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "winreg" 3 | version = "0.0.0" 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | description = "A small library for querying the Windows registry." 8 | categories = ["windows"] 9 | 10 | [target.'cfg(target_os = "windows")'.dependencies.windows-sys] 11 | version = "0.59.0" 12 | features = [ 13 | "Win32_Foundation", 14 | "Win32_System_Registry", 15 | ] 16 | -------------------------------------------------------------------------------- /proto/rrg/os.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.os; 8 | 9 | // List of all the operating systems supported by the agent. 10 | enum Type { 11 | UNKNOWN = 0; 12 | LINUX = 1; 13 | MACOS = 2; 14 | WINDOWS = 3; 15 | } 16 | -------------------------------------------------------------------------------- /crates/tsk-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tsk-sys" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [build-dependencies] 8 | bindgen = "0.71.1" 9 | cc = { version = "1.2.1" } 10 | 11 | # Parallel compilation on MSVC causes permission errors due to file locking. 12 | [target.'cfg(not(target_env = "msvc"))'.build-dependencies] 13 | cc = { version = "1.2.1", features = ["parallel"] } 14 | -------------------------------------------------------------------------------- /proto/rrg/action/list_mounts.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.list_mounts; 8 | 9 | import "rrg/fs.proto"; 10 | 11 | message Result { 12 | // Information about the individual filesystem mount. 13 | rrg.fs.Mount mount = 1; 14 | } 15 | -------------------------------------------------------------------------------- /proto/rrg/action/list_connections.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.list_connections; 8 | 9 | import "rrg/net.proto"; 10 | 11 | message Result { 12 | // Information about the individual connection. 13 | rrg.net.Connection connection = 1; 14 | } 15 | -------------------------------------------------------------------------------- /proto/rrg/action/list_interfaces.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.list_interfaces; 8 | 9 | import "rrg/net.proto"; 10 | 11 | message Result { 12 | // Information about the individual network interface. 13 | rrg.net.Interface interface = 1; 14 | } 15 | -------------------------------------------------------------------------------- /crates/wmi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wmi" 3 | version = "0.0.0" 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | description = "A small library for running WQL queries." 8 | categories = ["windows"] 9 | 10 | [target.'cfg(target_os = "windows")'.dependencies.windows-sys] 11 | version = "0.59.0" 12 | features = [ 13 | "Win32_Foundation", 14 | "Win32_System_Com", 15 | "Win32_System_Ole", 16 | "Win32_Security", 17 | "Win32_System_Variant", 18 | "Win32_System_Wmi", 19 | ] 20 | -------------------------------------------------------------------------------- /crates/rrg-proto/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rrg-proto" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies.ospect] 8 | path = "../ospect" 9 | 10 | [dependencies.winreg] 11 | path = "../winreg" 12 | 13 | [dependencies.log] 14 | version = "0.4.22" 15 | 16 | [dependencies.protobuf] 17 | version = "3.7.2" 18 | 19 | [dev-dependencies.quickcheck] 20 | version = "1.0.3" 21 | 22 | [build-dependencies.protobuf-codegen] 23 | version = "3.7.2" 24 | 25 | [build-dependencies.tempfile] 26 | version = "3.13.0" 27 | -------------------------------------------------------------------------------- /proto/rrg/action/list_utmp_users.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.list_utmp_users; 8 | 9 | import "rrg/fs.proto"; 10 | 11 | message Args { 12 | // Path to the file to use as a source for `utmp` records. 13 | // 14 | // Typically this should be `/var/log/wtmp`. 15 | rrg.fs.Path path = 1; 16 | } 17 | 18 | message Result { 19 | // Name of an individual user retrieved from `utmp` records. 20 | bytes username = 1; 21 | } 22 | -------------------------------------------------------------------------------- /crates/tsk-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | #![allow(non_upper_case_globals)] 6 | #![allow(non_camel_case_types)] 7 | #![allow(non_snake_case)] 8 | #![allow(rustdoc::broken_intra_doc_links)] 9 | #![allow(unsafe_op_in_unsafe_fn)] 10 | #![allow(unnecessary_transmutes)] 11 | // improper_ctypes triggers because TSK uses u128, which is FFI-safe with modern 12 | // LLVM. Remove this when https://github.com/rust-lang/rust/pull/137306 makes it 13 | // into the latest stable release. 14 | #![allow(improper_ctypes)] 15 | #![allow(clippy::all)] 16 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 17 | -------------------------------------------------------------------------------- /crates/rrg/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | // TODO: Hide irrelevant modules. 7 | 8 | pub mod action; 9 | pub mod args; 10 | pub mod fs; 11 | pub mod io; 12 | pub mod log; 13 | pub mod session; 14 | 15 | mod blob; 16 | mod filter; 17 | mod request; 18 | mod response; 19 | 20 | mod ping; 21 | mod startup; 22 | 23 | // TODO(@panhania): Consider moving this to a separate submodule. 24 | #[cfg(feature = "action-get_filesystem_timeline")] 25 | pub mod gzchunked; 26 | 27 | pub use ping::Ping; 28 | pub use startup::Startup; 29 | 30 | pub use request::{ParseRequestError, Request, RequestId}; 31 | pub use response::{Item, LogBuilder, Parcel, ResponseBuilder, ResponseId, Sink}; 32 | -------------------------------------------------------------------------------- /crates/ospect/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ospect" 3 | version = "0.0.0" 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | description = "A small library for inspecting the operating system." 8 | categories = ["os", "filesystem"] 9 | 10 | [features] 11 | test-setfattr = [] 12 | 13 | [dependencies.libc] 14 | version = "0.2.161" 15 | 16 | [target.'cfg(target_os = "windows")'.dependencies.windows-sys] 17 | version = "0.59.0" 18 | features = [ 19 | "Win32_Foundation", 20 | "Win32_NetworkManagement_IpHelper", 21 | "Win32_NetworkManagement_Ndis", 22 | "Win32_Networking_WinSock", 23 | "Win32_Storage_FileSystem", 24 | "Win32_System_SystemInformation", 25 | "Win32_System_LibraryLoader", 26 | "Win32_System_ProcessStatus", 27 | "Win32_System_Registry", 28 | ] 29 | 30 | [dev-dependencies.tempfile] 31 | version = "3.13.0" 32 | -------------------------------------------------------------------------------- /crates/rrg/src/ping.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | // TODO(@panhania): Remove once no longer needed. 7 | /// Ping message sent periodically to the GRR server. 8 | pub struct Ping { 9 | /// Time at which the message was (well, "is about to be") sent. 10 | pub sent: std::time::SystemTime, 11 | /// Increasing sequence number since the agent process was started. 12 | pub seq: u32, 13 | } 14 | 15 | impl crate::response::Item for Ping { 16 | 17 | type Proto = rrg_proto::ping::Ping; 18 | 19 | fn into_proto(self) -> rrg_proto::ping::Ping { 20 | let mut proto = rrg_proto::ping::Ping::new(); 21 | proto.set_send_time(rrg_proto::into_timestamp(self.sent)); 22 | proto.set_seq(self.seq); 23 | 24 | proto 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /proto/rrg/action/get_winreg_value.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.get_winreg_value; 8 | 9 | import "rrg/winreg.proto"; 10 | 11 | message Args { 12 | // Root predefined key of the value to get. 13 | rrg.winreg.PredefinedKey root = 1; 14 | 15 | // Key relative to `root` of the value to get (e.g. `SOFTWARE\Microsoft`). 16 | string key = 2; 17 | 18 | // Name of the value to get. 19 | string name = 3; 20 | } 21 | 22 | message Result { 23 | // Root predefined key of the retrieved value. 24 | rrg.winreg.PredefinedKey root = 1; 25 | 26 | // Key relative to `root` of the retrieved value. 27 | string key = 2; 28 | 29 | // Retrieved value. 30 | rrg.winreg.Value value = 3; 31 | } 32 | -------------------------------------------------------------------------------- /crates/rrg/src/blob.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | // Binary data object. 7 | pub struct Blob { 8 | // Binary data that the blob represents. 9 | data: Vec, 10 | } 11 | 12 | impl Blob { 13 | 14 | /// Extracts the slice of blob data bytes. 15 | pub fn as_bytes(&self) -> &[u8] { 16 | self.data.as_slice() 17 | } 18 | } 19 | 20 | impl From> for Blob { 21 | 22 | fn from(data: Vec) -> Blob { 23 | Blob { 24 | data, 25 | } 26 | } 27 | } 28 | 29 | impl crate::response::Item for Blob { 30 | 31 | type Proto = rrg_proto::blob::Blob; 32 | 33 | fn into_proto(self) -> Self::Proto { 34 | let mut proto = Self::Proto::default(); 35 | proto.set_data(self.data); 36 | 37 | proto 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /proto/rrg/action/list_winreg_values.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.list_winreg_values; 8 | 9 | import "rrg/winreg.proto"; 10 | 11 | message Args { 12 | // Root predefined key of the key to list values of. 13 | rrg.winreg.PredefinedKey root = 1; 14 | 15 | // Key relative to `root` to list values of. 16 | string key = 2; 17 | 18 | // Limit on the depth of recursion when visiting subkeys. 19 | // 20 | // The default value (0) means that only the values of the given are listed. 21 | uint32 max_depth = 3; 22 | } 23 | 24 | message Result { 25 | // Root predefined key of the listed value. 26 | rrg.winreg.PredefinedKey root = 1; 27 | 28 | // Key relative to `root` of the listed value. 29 | string key = 2; 30 | 31 | // Listed value. 32 | rrg.winreg.Value value = 3; 33 | } 34 | -------------------------------------------------------------------------------- /proto/rrg/ping.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.ping; 8 | 9 | import "google/protobuf/timestamp.proto"; 10 | 11 | // Ping message sent pariodically to the GRR server. 12 | // 13 | // This is intended to be used as a workaround to the GRR fleet collection sche- 14 | // duling mechanism. In the current implementations it is the agents that ask 15 | // for work, not the other way around. While it should be refactored in the 16 | // future, re-implementing this Python agent quirk is a quick way to unblock 17 | // agent migration. 18 | // 19 | // TODO(@panhania): Remove once no longer needed. 20 | message Ping { 21 | // Time at which the message was sent. 22 | google.protobuf.Timestamp send_time = 1; 23 | 24 | // Increasing sequence number since the agent process was started. 25 | // 26 | // Starts at 0. 27 | uint32 seq = 3; 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Google LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /proto/rrg/blob.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.fs; 8 | 9 | // Binary data object. 10 | // 11 | // Blobs are used mostly (but not exclusively) to represent contents of files: 12 | // sending an entire file in one Protocol Buffers message is not feasible, so we 13 | // divide files to smaller portions and deliver it to the server one by one. 14 | // 15 | // On the server, the blob sink stores blobs immediately in a blobstore instead 16 | // of storing it in the database to be later picked up by a worker for further 17 | // processing. 18 | message Blob { 19 | // Binary data that the blob represents. 20 | bytes data = 1; 21 | 22 | // TODO: Consider adding compression. 23 | // 24 | // When transferring blobs, GRR offers optional compression layer. This is not 25 | // that important since messages sent by Fleetspeak are compressed anyway, but 26 | // may reduce amount of communication to and from Fleetspeak. 27 | } 28 | -------------------------------------------------------------------------------- /proto/rrg/action/get_tcp_response.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.get_tcp_response; 8 | 9 | import "google/protobuf/duration.proto"; 10 | import "rrg/net.proto"; 11 | 12 | message Args { 13 | // Address of the host to connect to. 14 | rrg.net.SocketAddress address = 1; 15 | 16 | // Timeout for establishing the connection with the host. 17 | google.protobuf.Duration connect_timeout = 2; 18 | 19 | // Timeout for writing data to the TCP stream. 20 | google.protobuf.Duration write_timeout = 3; 21 | 22 | // Timeout for reading data from the TCP stream. 23 | google.protobuf.Duration read_timeout = 4; 24 | 25 | // Data to write to the TCP stream. 26 | bytes data = 5; 27 | } 28 | 29 | message Result { 30 | // Data read from the TCP stream. 31 | bytes data = 1; 32 | 33 | // TODO(@panhania): Consider whether we should include a `data_truncated` 34 | // field to indicate whether there was more data available. 35 | } 36 | -------------------------------------------------------------------------------- /proto/rrg/action/list_winreg_keys.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.list_winreg_keys; 8 | 9 | import "rrg/winreg.proto"; 10 | import "google/protobuf/timestamp.proto"; 11 | 12 | message Args { 13 | // Root predefined key of the key to list subkeys of. 14 | rrg.winreg.PredefinedKey root = 1; 15 | 16 | // Key relative to `root` to list subkeys of. 17 | string key = 2; 18 | 19 | // Limit on the depth of recursion when visiting subkeys. 20 | // 21 | // The default value (0) is treated the same as value of 1, meaning only the 22 | // immediate subkeys will be listed. 23 | uint32 max_depth = 3; 24 | } 25 | 26 | message Result { 27 | // Root predefined key of the listed subkey. 28 | rrg.winreg.PredefinedKey root = 1; 29 | 30 | // Key relative to `root` of the listed subkey. 31 | string key = 2; 32 | 33 | // Listed subkey relative to `root` and `key`. 34 | string subkey = 3; 35 | 36 | // Last modification time of the listed subkey. 37 | google.protobuf.Timestamp modification_time = 4; 38 | } 39 | -------------------------------------------------------------------------------- /crates/ospect/src/os/linux.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | /// Returns the time at which the system was installed. 7 | pub fn installed() -> std::io::Result { 8 | crate::os::unix::installed() 9 | } 10 | 11 | /// Returns the [`Kind`] of currently running operating system. 12 | /// 13 | /// [`Kind`]: crate::os::Kind 14 | pub fn kind() -> crate::os::Kind { 15 | crate::os::Kind::Linux 16 | } 17 | 18 | /// Returns the version string of the currently running operating system. 19 | pub fn version() -> std::io::Result { 20 | crate::os::unix::version() 21 | } 22 | 23 | /// Returns the CPU architecture of the currently running operating system. 24 | pub fn arch() -> std::io::Result { 25 | crate::os::unix::arch() 26 | } 27 | 28 | /// Returns the hostname of the currently running operating system. 29 | pub fn hostname() -> std::io::Result { 30 | crate::os::unix::hostname() 31 | } 32 | 33 | /// Returns the FQDN of the currently running operating system. 34 | pub fn fqdn() -> std::io::Result { 35 | crate::os::unix::fqdn() 36 | } 37 | -------------------------------------------------------------------------------- /proto/rrg/action/grep_file_contents.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.grep_file_contents; 8 | 9 | import "rrg/fs.proto"; 10 | 11 | message Args { 12 | // Absolute path to the file to grep the contents of. 13 | // 14 | // The file content must be valid UTF-8. 15 | rrg.fs.Path path = 1; 16 | 17 | // Regular expression to search for in the file contents. 18 | // 19 | // The specific syntax of the regex language is left unspecified as the 20 | // implementation detail but most common regex features can be expected to 21 | // be supported. 22 | string regex = 2; 23 | 24 | // TODO(@panhania): Add support for files that not necessarily conform to 25 | // Unicode. 26 | 27 | // TODO(@panhania): Add support for different file encodings. 28 | } 29 | 30 | message Result { 31 | // Byte offset within the file from which the content matched. 32 | uint64 offset = 1; 33 | 34 | // Content that matched the specified regular expression. 35 | string content = 2; 36 | 37 | // TODO(@panhania): Add support for capture groups. 38 | } 39 | -------------------------------------------------------------------------------- /crates/ospect/src/net/unix.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | /// Returns an iterator over IPv4 TCP connections of all processes. 4 | pub fn all_tcp_v4_connections() -> std::io::Result>> { 5 | let pids = crate::proc::ids()?; 6 | Ok(pids.flat_map(|pid| crate::net::tcp_v4_connections(pid?)).flatten()) 7 | } 8 | 9 | /// Returns an iterator over IPv6 TCP connections of all processes. 10 | pub fn all_tcp_v6_connections() -> std::io::Result>> { 11 | let pids = crate::proc::ids()?; 12 | Ok(pids.flat_map(|pid| crate::net::tcp_v6_connections(pid?)).flatten()) 13 | } 14 | 15 | /// Returns an iterator over IPv4 UDP connections of all processes. 16 | pub fn all_udp_v4_connections() -> std::io::Result>> { 17 | let pids = crate::proc::ids()?; 18 | Ok(pids.flat_map(|pid| crate::net::udp_v4_connections(pid?)).flatten()) 19 | } 20 | 21 | /// Returns an iterator over IPv6 UDP connections of all processes. 22 | pub fn all_udp_v6_connections() -> std::io::Result>> { 23 | let pids = crate::proc::ids()?; 24 | Ok(pids.flat_map(|pid| crate::net::udp_v6_connections(pid?)).flatten()) 25 | } 26 | -------------------------------------------------------------------------------- /crates/ospect/src/proc.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | #[cfg(target_os = "linux")] 7 | mod linux; 8 | 9 | #[cfg(target_os = "macos")] 10 | mod macos; 11 | 12 | #[cfg(target_os = "windows")] 13 | mod windows; 14 | 15 | mod sys { 16 | #[cfg(target_os = "linux")] 17 | pub use crate::proc::linux::*; 18 | 19 | #[cfg(target_os = "macos")] 20 | pub use crate::proc::macos::*; 21 | 22 | #[cfg(target_os = "windows")] 23 | pub use crate::proc::windows::*; 24 | } 25 | 26 | /// Returns an iterator yielding identifiers of all processes on the system. 27 | /// 28 | /// The order in which the identifiers are yield is not defined. 29 | /// 30 | /// # Errors 31 | /// 32 | /// The function will return an error if the operating system does not allow 33 | /// get the required information (e.g. in case of insufficient permissions). 34 | /// 35 | /// # Examples 36 | /// 37 | /// ``` 38 | /// let mut pids = ospect::proc::ids() 39 | /// .unwrap(); 40 | /// 41 | /// assert! { 42 | /// pids.find(|pid| *pid.as_ref().unwrap() == std::process::id()).is_some() 43 | /// }; 44 | /// ``` 45 | pub fn ids() -> std::io::Result>> { 46 | self::sys::ids() 47 | } 48 | -------------------------------------------------------------------------------- /proto/rrg/action/get_file_sha256.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.get_file_sha256; 8 | 9 | import "rrg/fs.proto"; 10 | 11 | message Args { 12 | // Absolute path to the file to get the SHA-256 hash of. 13 | rrg.fs.Path path = 1; 14 | 15 | // Byte offset from which the content should be hashed. 16 | // 17 | // If unset, hashes from the beginning of the file. 18 | uint64 offset = 2; 19 | 20 | // Number of bytes to hash (from the start offset). 21 | // 22 | // This is value serves as an upper bound: if the file is shorter than the 23 | // specified length, only bytes that actually exist are going to be hashed. 24 | // 25 | // If unset, hashes until the end of the file. 26 | uint64 length = 3; 27 | } 28 | 29 | message Result { 30 | // Absolute path of the file this result corresponds to. 31 | rrg.fs.Path path = 1; 32 | 33 | // Byte offset from which the file content was hashed. 34 | uint64 offset = 2; 35 | 36 | // Number of bytes of the file used to produce the hash. 37 | uint64 length = 3; 38 | 39 | // SHA-256 [1] hash digest of the file content. 40 | // 41 | // [1]: https://en.wikipedia.org/wiki/SHA-2 42 | bytes sha256 = 4; 43 | } 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | ## Contributor License Agreement 5 | 6 | Contributions to this project must be accompanied by a Contributor License 7 | Agreement (CLA). You (or your employer) retain the copyright to your 8 | contribution; this simply gives us permission to use and redistribute your 9 | contributions as part of the project. Head over to 10 | to see your current agreements on file or 11 | to sign a new one. 12 | 13 | You generally only need to submit a CLA once, so if you've already submitted one 14 | (even if it was for a different project), you probably don't need to do it 15 | again. 16 | 17 | ## Style guide 18 | 19 | This project follows the official Rust [style guidelines][rust-style] and all 20 | code should be written with them in mind. Using tools such as [Clippy][clippy] 21 | and [Rustfmt][rustfmt] can be very helpful with this. 22 | 23 | [rust-style]: https://doc.rust-lang.org/1.0.0/style/ 24 | [clippy]: https://github.com/rust-lang/rust-clippy 25 | [rustfmt]: https://github.com/rust-lang/rustfmt 26 | 27 | ## Code reviews 28 | 29 | In order to submit new code to this repository a code review is needed. Follow 30 | the [GitHub Help][github-pr] guide to learn about the pull request process. 31 | 32 | [github-pr]: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests 33 | -------------------------------------------------------------------------------- /proto/rrg/action/get_file_contents.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.get_file_contents; 8 | 9 | import "rrg/fs.proto"; 10 | 11 | message Args { 12 | // Absolute paths to the file to get the contents of. 13 | repeated rrg.fs.Path paths = 1; 14 | 15 | // Byte offset from which the content should be retrieved. 16 | // 17 | // If unset, starts from the beginning of the file. 18 | uint64 offset = 2; 19 | 20 | // Number of bytes to from the file from the given offset to fetch. 21 | // 22 | // If unset, collects the entire file (possibly in multible results). 23 | uint64 length = 3; 24 | } 25 | 26 | message Result { 27 | // Path to the file this result corresponds to. 28 | rrg.fs.Path path = 4; 29 | 30 | // A byte offset of the file part sent to the blob sink. 31 | // 32 | // Set only if `error` is not set. 33 | uint64 offset = 1; 34 | 35 | // A number of bytes of the file part sent to the blob sink. 36 | // 37 | // Set only if `error` is not set. 38 | uint64 length = 2; 39 | 40 | // A SHA-256 hash of the file part sent to the blob sink. 41 | // 42 | // Set only if `error` is not set. 43 | bytes blob_sha256 = 3; 44 | 45 | // Error message set if something went wrong when processing the file. 46 | string error = 5; 47 | } 48 | -------------------------------------------------------------------------------- /proto/rrg/action/get_filesystem_timeline_tsk.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.get_filesystem_timeline_tsk; 8 | 9 | import "rrg/fs.proto"; 10 | 11 | message Args { 12 | // Optional. Absolute path to the raw filesystem image to delve into. If 13 | // unset, RRG will attempt to detect the path to the raw filesystem image 14 | // based on system mountpoints. 15 | rrg.fs.Path raw_fs = 1; 16 | // Absolute path to the root directory in the image to get the timeline of. 17 | rrg.fs.Path root = 2; 18 | } 19 | 20 | message Result { 21 | // A SHA-256 hash of the timeline batch sent to the blob sink. 22 | // 23 | // Because the entire timeline can easily have millions of entries, it could 24 | // quickly exceed the maximum allowed size for a message. This is why entries 25 | // are batched, gzipped and then send as blobs to the blobstore. 26 | bytes blob_sha256 = 1; 27 | 28 | // The total number of entries in the chunk. 29 | // 30 | // This number includes only entries contained in the chunk corresponding to 31 | // this result, not the total number of entries the action execution processed 32 | // so far. 33 | uint64 entry_count = 2; 34 | } 35 | 36 | // Note: this action produces timeline batches serialized with 37 | // rrg.action.get_filesystem_timeline.Entry 38 | -------------------------------------------------------------------------------- /proto/rrg/action/get_system_metadata.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.get_system_metadata; 8 | 9 | import "google/protobuf/timestamp.proto"; 10 | import "rrg/os.proto"; 11 | 12 | message Args { 13 | } 14 | 15 | message Result { 16 | // The type of the operating system. 17 | rrg.os.Type type = 1; 18 | 19 | // A system version string (e.g. `Darwin Kernel Version 21.6.0: Mon Dec 19 20 | // 20:44:01 PST 2022; root:xnu-8020.240.18~2/RELEASE_X86_64`). 21 | // 22 | // No assumptions on the specific format of this string should be made. 23 | string version = 2; 24 | 25 | // CPU architecture of the operating system (e.g. `x86_64`). 26 | // 27 | // No assumptions on the specific format of this string should be made. 28 | string arch = 6; 29 | 30 | // The hostname of the operating system. 31 | string hostname = 4; 32 | 33 | // The FQDN of the operating system. 34 | // 35 | // Note that depending on the specific operating system configuration this 36 | // value might be sometimes reported as just a hostname. 37 | string fqdn = 5; 38 | 39 | // The time at which the operating system was installed. 40 | // 41 | // Note that this data is based on various heuristics and might not be very 42 | // accurate. 43 | google.protobuf.Timestamp install_time = 3; 44 | } 45 | -------------------------------------------------------------------------------- /proto/rrg/action/query_wmi.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | syntax = "proto3"; 7 | 8 | package rrg.action.query_wmi; 9 | 10 | message Args { 11 | // WQL query [1] to run. 12 | // 13 | // [1]: https://learn.microsoft.com/en-us/windows/win32/wmisdk/wql-sql-for-wmi 14 | string query = 1; 15 | /// WMI namespace object path [1] to use for the query. 16 | /// 17 | /// [1]: https://learn.microsoft.com/en-us/windows/win32/wmisdk/describing-a-wmi-namespace-object-path 18 | string namespace = 2; 19 | } 20 | 21 | message Result { 22 | // Single row of the query result mapping column names to their values. 23 | map row = 1; 24 | } 25 | 26 | message Value { 27 | oneof value { 28 | // Boolean value. 29 | bool bool = 1; 30 | 31 | // Unsigned integer. 32 | // 33 | // 8-bit, 16-bit, 32-bit and 64-bit unsigned integers are mapped to this 34 | // field. 35 | uint64 uint = 2; 36 | 37 | // Signed integer. 38 | // 39 | // 8-bit, 16-bit, 32-bit and 64-bit signed integers are mapped to this 40 | // field. 41 | int64 int = 3; 42 | 43 | // Single-precision floating point number. 44 | float float = 4; 45 | 46 | // Double-precision floating point number. 47 | double double = 5; 48 | 49 | // String value. 50 | string string = 6; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/ospect/src/os/macos.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | /// Returns the time at which the system was installed. 7 | pub fn installed() -> std::io::Result { 8 | crate::os::unix::installed() 9 | } 10 | 11 | /// Returns the [`Kind`] of currently running operating system. 12 | /// 13 | /// [`Kind`]: crate::os::Kind 14 | pub fn kind() -> crate::os::Kind { 15 | crate::os::Kind::Macos 16 | } 17 | 18 | /// Returns the version string of the currently running operating system. 19 | pub fn version() -> std::io::Result { 20 | crate::os::unix::version() 21 | } 22 | 23 | /// Returns the CPU architecture of the currently running operating system. 24 | pub fn arch() -> std::io::Result { 25 | crate::os::unix::arch() 26 | } 27 | 28 | /// Returns the hostname of the currently running operating system. 29 | pub fn hostname() -> std::io::Result { 30 | crate::os::unix::hostname() 31 | } 32 | 33 | /// Returns the FQDN of the currently running operating system. 34 | /// Returns the hostname if it already contains a dot. 35 | pub fn fqdn() -> std::io::Result { 36 | let hostname = crate::os::unix::hostname()?; 37 | 38 | // If the hostname contains a dot, it's likely already a FQDN. 39 | // It might not be resolvable though so calling 40 | // crate::os::unix::fqdn() could fail. 41 | use std::os::unix::ffi::OsStrExt; 42 | if hostname.as_bytes().contains(&b'.') { 43 | return Ok(hostname); 44 | } 45 | 46 | crate::os::unix::fqdn() 47 | } 48 | -------------------------------------------------------------------------------- /crates/rrg/src/action/list_mounts.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | /// A result of the `list_mounts` action. 7 | struct Item { 8 | // Information about the individual filesystem mount. 9 | mount: ospect::fs::Mount, 10 | } 11 | 12 | // Handles invocations of the `list_mounts` action. 13 | pub fn handle(session: &mut S, _: ()) -> crate::session::Result<()> 14 | where 15 | S: crate::session::Session, 16 | { 17 | let mounts = ospect::fs::mounts() 18 | .map_err(crate::session::Error::action)?; 19 | 20 | for mount in mounts { 21 | let mount = match mount { 22 | Ok(mount) => mount, 23 | Err(error) => { 24 | log::warn!("failed to obtain mount information: {}", error); 25 | continue; 26 | } 27 | }; 28 | 29 | session.reply(Item { 30 | mount, 31 | })?; 32 | } 33 | 34 | Ok(()) 35 | } 36 | 37 | impl crate::response::Item for Item { 38 | 39 | type Proto = rrg_proto::list_mounts::Result; 40 | 41 | fn into_proto(self) -> rrg_proto::list_mounts::Result { 42 | let mut proto = rrg_proto::list_mounts::Result::default(); 43 | proto.set_mount(self.mount.into()); 44 | 45 | proto 46 | } 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | 52 | use super::*; 53 | 54 | #[test] 55 | fn handle_some_mount() { 56 | let mut session = crate::session::FakeSession::new(); 57 | assert!(handle(&mut session, ()).is_ok()); 58 | 59 | assert!(session.reply_count() > 0); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/rrg-proto/src/convert.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | //! Traits for conversions between types. 7 | //! 8 | //! This module provides utility traits similar to what `std::convert` does and 9 | //! can be thought as an extension of it to fit RRG-specific purposes. 10 | 11 | /// A lossy conversion from one type to the other. 12 | /// 13 | /// This trait is very similar to `From` from the standard library, except that 14 | /// it allows values to lose some information. Moreover, the implementers are 15 | /// allowed to log details about the lost information, so the conversion might 16 | /// have side effects. 17 | /// 18 | /// See also [`IntoLossy`]. 19 | /// 20 | /// [`IntoLossy`]: trait.IntoLossy.html 21 | pub trait FromLossy: Sized { 22 | /// Convert the value of another type. 23 | fn from_lossy(_: T) -> Self; 24 | } 25 | 26 | /// A lossy conversion into one type from the other. 27 | /// 28 | /// This trait is very similar to `Into` from the standard library, except that 29 | /// it allows values to lose some information. Moreover, the implementers are 30 | /// allowed to log details about the lost information, so the conversion might 31 | /// have side effects. 32 | /// 33 | /// Note that it is discouraged to implement this trait directly. Instead, one 34 | /// should provide a reverse implementation for [`FromLossy`] and derive the 35 | /// implementation for `IntoLossy` automatically. 36 | /// 37 | /// [`FromLossy`]: trait.FromLossy.html 38 | pub trait IntoLossy: Sized { 39 | /// Convert the value into another type. 40 | fn into_lossy(self) -> T; 41 | } 42 | 43 | impl IntoLossy for T 44 | where 45 | U: FromLossy, 46 | { 47 | 48 | fn into_lossy(self) -> U { 49 | U::from_lossy(self) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/winreg/src/path.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | /// Adjoins `left` and `right` using Windows registry key separator (`\`). 7 | /// 8 | /// This is simlar to [`std::path::Path::join`] but for Windows registry keys. 9 | /// 10 | /// # Examples 11 | /// 12 | /// ``` 13 | /// assert_eq!(winreg::path::join("SOFTWARE", ""), "SOFTWARE"); 14 | /// assert_eq!(winreg::path::join("SOFTWARE", "Windows"), "SOFTWARE\\Windows"); 15 | /// ``` 16 | pub fn join(left: S, right: S) -> std::ffi::OsString 17 | where 18 | S: AsRef, 19 | { 20 | let left = left.as_ref(); 21 | let right = right.as_ref(); 22 | 23 | let mut result = std::ffi::OsString::new(); 24 | result.push(left); 25 | if !left.is_empty() && !right.is_empty() { 26 | result.push("\\"); 27 | } 28 | result.push(right); 29 | result 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | 35 | use super::*; 36 | 37 | #[test] 38 | fn join_both_empty() { 39 | let empty = std::ffi::OsString::new(); 40 | assert_eq!(join(&empty, &empty), ""); 41 | } 42 | 43 | #[test] 44 | fn join_left_empty() { 45 | let empty = std::ffi::OsString::new(); 46 | let foo = std::ffi::OsString::from("foo"); 47 | assert_eq!(join(&empty, &foo), "foo"); 48 | } 49 | 50 | #[test] 51 | fn join_right_empty() { 52 | let empty = std::ffi::OsString::new(); 53 | let foo = std::ffi::OsString::from("foo"); 54 | assert_eq!(join(&foo, &empty), "foo"); 55 | } 56 | 57 | #[test] 58 | fn join_both_not_empty() { 59 | let foo = std::ffi::OsString::from("foo"); 60 | let bar = std::ffi::OsString::from("bar"); 61 | assert_eq!(join(&foo, &bar), "foo\\bar"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /proto/rrg/winreg.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.winreg; 8 | 9 | // [Predefined key][1] of the Windows registry. 10 | // 11 | // Note that the integer representation **does not** correspond to the `HKEY_*` 12 | // constants as defined in the [`winreg.h`] header (they are out of the allowed 13 | // range for Protocol Buffer enums). 14 | // 15 | // [1]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/predefined-keys 16 | enum PredefinedKey { 17 | UNKNOWN = 0; 18 | CLASSES_ROOT = 1; 19 | CURRENT_USER = 2; 20 | LOCAL_MACHINE = 3; 21 | USERS = 4; 22 | PERFORMANCE_DATA = 5; 23 | CURRENT_CONFIG = 6; 24 | PERFORMANCE_TEXT = 7; 25 | PERFORMANCE_NLSTEXT = 8; 26 | CURRENT_USER_LOCAL_SETTINGS = 9; 27 | } 28 | 29 | // [Value][1] of the Windows registry. 30 | // 31 | // [1]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-value-types 32 | message Value { 33 | // Name of the value. 34 | string name = 1; 35 | 36 | // Data associated with the value. 37 | oneof data { 38 | // Byte string. 39 | bytes bytes = 2; 40 | // Unicode string. 41 | string string = 3; 42 | // Unicode string with unexpanded references to environment variables. 43 | string expand_string = 4; 44 | // Sequence of unicode strings. 45 | StringList multi_string = 5; 46 | // Symbolic link to another registry key. 47 | string link = 6; 48 | // 32-bit number. 49 | uint32 uint32 = 7; 50 | // 64-bit number. 51 | uint64 uint64 = 8; 52 | } 53 | 54 | // Wrapper for list of strings to be used in `oneof` fields. 55 | message StringList { 56 | // Actual list of strings. 57 | repeated string values = 1; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'Integrate' 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | ci: 7 | name: 'CI' 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: 13 | - ubuntu-latest 14 | - ubuntu-24.04-arm 15 | - macos-latest 16 | - windows-latest 17 | toolchain: 18 | - stable 19 | - nightly 20 | steps: 21 | - name: 'Install Linux dependencies' 22 | if: ${{ runner.os == 'Linux' }} 23 | run: | 24 | sudo apt update 25 | sudo apt install attr e2fsprogs libfuse-dev libguestfs-tools 26 | # The following is needed for `guestmount` (from `libguestfs-tools`) 27 | # to work on Ubuntu [1, 2]. 28 | # 29 | # [1]: https://bugs.launchpad.net/ubuntu/+source/linux/+bug/759725 30 | # [2]: https://askubuntu.com/questions/1046828/how-to-run-libguestfs-tools-tools-such-as-virt-make-fs-without-sudo 31 | sudo chmod +r /boot/vmlinuz* 32 | - name: 'Install macOS dependencies' 33 | if: ${{ runner.os == 'macOS' }} 34 | run: brew install autoconf automake libtool 35 | - name: 'Checkout the repository' 36 | uses: actions/checkout@v2 37 | with: 38 | submodules: true 39 | - name: 'Setup the Rust toolchain' 40 | run: | 41 | rustup update ${{ matrix.toolchain }} 42 | rustup override set ${{ matrix.toolchain }} 43 | rustup --version 44 | rustc --version 45 | cargo --version 46 | - name: 'Build RRG executable' 47 | run: cargo build --features 'action-get_file_contents_kmx action-get_filesystem_timeline_tsk' 48 | # TODO: Add a step that runs tests with all action features disabled. 49 | - name: 'Run RRG tests' 50 | run: cargo test --features 'test-chattr test-setfattr test-fuse test-wtmp test-libguestfs action-get_file_contents_kmx action-get_filesystem_timeline_tsk' 51 | -------------------------------------------------------------------------------- /proto/rrg/action/get_file_contents_kmx.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.get_file_contents_kmx; 8 | 9 | import "rrg/fs.proto"; 10 | 11 | message Args { 12 | oneof volume { 13 | // Absolute path to the raw volume holding the files to get the contents of. 14 | // 15 | // For example, `\\?\Volume{f000f000-baa5-f000-baa5-f00ba5f00ba5}`. 16 | rrg.fs.Path volume_path = 4; 17 | 18 | // Absolute path to the point (e.g. a drive letter) where a volume holding 19 | // the files to get the contents of is mounted. 20 | // 21 | // For example, `C:\`. 22 | // 23 | // This is supported only on Windows. 24 | rrg.fs.Path volume_mount_path = 5; 25 | } 26 | 27 | // Paths (relative to the volume root) to the files to get the contents of. 28 | repeated rrg.fs.Path paths = 1; 29 | 30 | // Byte offset from which the content should be retrieved. 31 | // 32 | // If unset, starts from the beginning of the file. 33 | uint64 offset = 2; 34 | 35 | // Number of bytes to from the file from the given offset to fetch. 36 | // 37 | // If unset, collects the entire file (possibly in multible results). 38 | uint64 length = 3; 39 | } 40 | 41 | message Result { 42 | // Path to the file this result corresponds to. 43 | rrg.fs.Path path = 4; 44 | 45 | // A byte offset of the file part sent to the blob sink. 46 | // 47 | // Set only if `error` is not set. 48 | uint64 offset = 1; 49 | 50 | // A number of bytes of the file part sent to the blob sink. 51 | // 52 | // Set only if `error` is not set. 53 | uint64 length = 2; 54 | 55 | // A SHA-256 hash of the file part sent to the blob sink. 56 | // 57 | // Set only if `error` is not set. 58 | bytes blob_sha256 = 3; 59 | 60 | // Error message set if something went wrong when processing the file. 61 | string error = 5; 62 | } 63 | -------------------------------------------------------------------------------- /crates/rrg/src/action/list_interfaces.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | /// A result of the `list_interfaces` action. 7 | struct Item { 8 | // Information about the individual network interface. 9 | iface: ospect::net::Interface, 10 | } 11 | 12 | // Handles invocations of the `list_interfaces` action. 13 | pub fn handle(session: &mut S, _: ()) -> crate::session::Result<()> 14 | where 15 | S: crate::session::Session, 16 | { 17 | let ifaces = ospect::net::interfaces() 18 | .map_err(crate::session::Error::action)?; 19 | 20 | for iface in ifaces { 21 | session.reply(Item { 22 | iface, 23 | })?; 24 | } 25 | 26 | Ok(()) 27 | } 28 | 29 | impl crate::response::Item for Item { 30 | 31 | type Proto = rrg_proto::list_interfaces::Result; 32 | 33 | fn into_proto(self) -> rrg_proto::list_interfaces::Result { 34 | let mut proto = rrg_proto::list_interfaces::Result::default(); 35 | proto.set_interface(self.iface.into()); 36 | 37 | proto 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | 44 | use super::*; 45 | 46 | #[test] 47 | // Loopback interface is not available on Windows. 48 | #[cfg_attr(target_family = "windows", ignore)] 49 | fn handle_loopback_interface() { 50 | let mut session = crate::session::FakeSession::new(); 51 | assert!(handle(&mut session, ()).is_ok()); 52 | 53 | // A single network interface can be associated with many IP addresses, 54 | // some of which might not be traditional loopback addresses. Therefore, 55 | // we use `any` instead of `all`. 56 | fn is_loopback(iface: &ospect::net::Interface) -> bool { 57 | iface.ip_addrs().any(std::net::IpAddr::is_loopback) 58 | } 59 | 60 | assert! { 61 | session.replies().any(|item: &Item| is_loopback(&item.iface)) 62 | } 63 | } 64 | 65 | #[test] 66 | fn handle_some_interface() { 67 | let mut session = crate::session::FakeSession::new(); 68 | assert!(handle(&mut session, ()).is_ok()); 69 | 70 | assert!(session.reply_count() > 0); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /proto/rrg/startup.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.startup; 8 | 9 | import "google/protobuf/timestamp.proto"; 10 | import "rrg/fs.proto"; 11 | import "rrg/os.proto"; 12 | 13 | // Information about the agent startup. 14 | // 15 | // This message should be sent to the `STARTUP` sink. It should be sent only 16 | // once: at the moment the agent process is started. 17 | message Startup { 18 | // Metadata about the agent that has been started. 19 | Metadata metadata = 1; 20 | // Path to the agent's executable that is running. 21 | rrg.fs.Path path = 5; 22 | // Value of the command-line arguments the agent was invoked with. 23 | repeated string args = 2; 24 | // Time at which the agent was started. 25 | google.protobuf.Timestamp agent_startup_time = 3; 26 | // Time at which the operating system booted. 27 | google.protobuf.Timestamp os_boot_time = 4; 28 | // Type of the operating system. 29 | rrg.os.Type os_type = 6; 30 | } 31 | 32 | // Metadata about the RRG agent. 33 | message Metadata { 34 | // Name of the agent (should always be "RRG"). 35 | string name = 1; 36 | // Version of the agent. 37 | Version version = 3; 38 | // The time at which the agent executable was built. 39 | google.protobuf.Timestamp build_time = 4; 40 | } 41 | 42 | // Descriptor of the version. 43 | // 44 | // RRG uses [semantic versioning][semver], so refer to the specification for the 45 | // details. 46 | // 47 | // [semver]: https://semver.org/ 48 | message Version { 49 | // Major component of the version (`x` in `x.y.z`). 50 | uint32 major = 1; 51 | // Minor component of the version (`y` in `x.y.z`). 52 | uint32 minor = 2; 53 | // Patch component of the version (`z` in `x.y.z`). 54 | uint32 patch = 3; 55 | // Optional pre-release label of the version (`foo` in `x.y.z-foo`). 56 | // 57 | // This is a rather free-form identifier that can also include numbers and 58 | // dots (e.g. `beta.1`). See the [syntax] description of the Rust [`semver`] 59 | // crate for exact specification. 60 | // 61 | // [syntax]: https://docs.rs/semver/latest/semver/struct.Prerelease.html#syntax 62 | string pre = 4; 63 | } 64 | -------------------------------------------------------------------------------- /crates/rrg/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | use log::{error, info}; 7 | 8 | fn main() { 9 | let args = rrg::args::from_env_args(); 10 | rrg::log::init(&args); 11 | 12 | // TODO: https://github.com/rust-lang/rust/issues/92649 13 | // 14 | // Refactor once `panic_update_hook` is stable. 15 | 16 | // Because Fleetspeak does not necessarily capture RRG's standard error, it 17 | // might be difficult to find reason behind a crash. Thus, we extend the 18 | // standard panic hook to also log the panic message. 19 | let panic_hook = std::panic::take_hook(); 20 | std::panic::set_hook(Box::new(move |info| { 21 | // Note that logging is an I/O operation and it itself might panic. In 22 | // case of the logging failure it does not end in an endless cycle (of 23 | // trying to log, which panics, which tries to log and so on) but it 24 | // triggers an abort which is fine. 25 | error!("thread panicked: {info}"); 26 | panic_hook(info) 27 | })); 28 | 29 | info!("sending Fleetspeak startup information"); 30 | fleetspeak::startup(env!("CARGO_PKG_VERSION")); 31 | 32 | info!("sending RRG startup information"); 33 | rrg::Parcel::new(rrg::Sink::Startup, rrg::Startup::now()) 34 | .send_unaccounted(); 35 | 36 | // TODO(@panhania): Remove once no longer needed. 37 | if args.ping_rate > std::time::Duration::ZERO { 38 | std::thread::spawn(move || { 39 | info!("starting the pinging thread"); 40 | 41 | for seq in 0.. { 42 | info!("sending a ping message (seq: {seq})"); 43 | 44 | rrg::Parcel::new(rrg::Sink::Ping, rrg::Ping { 45 | sent: std::time::SystemTime::now(), 46 | seq, 47 | }).send_unaccounted(); 48 | 49 | std::thread::sleep(args.ping_rate); 50 | } 51 | }); 52 | } else { 53 | info!("pinging thread is disabled"); 54 | } 55 | 56 | info!("listening for messages"); 57 | loop { 58 | let request = rrg::Request::receive(args.heartbeat_rate); 59 | rrg::session::FleetspeakSession::dispatch(&args, request); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/rrg-proto/build.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | use std::path::PathBuf; 7 | 8 | const PROTOS: &'static [&'static str] = &[ 9 | "../../proto/rrg.proto", 10 | "../../proto/rrg/blob.proto", 11 | "../../proto/rrg/fs.proto", 12 | "../../proto/rrg/net.proto", 13 | "../../proto/rrg/os.proto", 14 | "../../proto/rrg/ping.proto", 15 | "../../proto/rrg/startup.proto", 16 | "../../proto/rrg/winreg.proto", 17 | "../../proto/rrg/action/execute_signed_command.proto", 18 | "../../proto/rrg/action/get_file_contents.proto", 19 | "../../proto/rrg/action/get_file_contents_kmx.proto", 20 | "../../proto/rrg/action/get_file_sha256.proto", 21 | "../../proto/rrg/action/get_file_metadata.proto", 22 | "../../proto/rrg/action/get_filesystem_timeline.proto", 23 | "../../proto/rrg/action/get_filesystem_timeline_tsk.proto", 24 | "../../proto/rrg/action/get_system_metadata.proto", 25 | "../../proto/rrg/action/get_tcp_response.proto", 26 | "../../proto/rrg/action/get_winreg_value.proto", 27 | "../../proto/rrg/action/grep_file_contents.proto", 28 | "../../proto/rrg/action/list_connections.proto", 29 | "../../proto/rrg/action/list_interfaces.proto", 30 | "../../proto/rrg/action/list_mounts.proto", 31 | "../../proto/rrg/action/list_utmp_users.proto", 32 | "../../proto/rrg/action/list_winreg_keys.proto", 33 | "../../proto/rrg/action/list_winreg_values.proto", 34 | "../../proto/rrg/action/query_wmi.proto", 35 | "../../proto/rrg/action/dump_process_memory.proto", 36 | "../../proto/rrg/action/scan_memory_yara.proto", 37 | ]; 38 | 39 | fn main() { 40 | let outdir: PathBuf = std::env::var("OUT_DIR") 41 | .expect("no output directory") 42 | .into(); 43 | 44 | for proto in PROTOS { 45 | println!("cargo:rerun-if-changed={}", proto); 46 | } 47 | 48 | let proto_out_dir = outdir.join("proto"); 49 | std::fs::create_dir_all(&proto_out_dir).unwrap(); 50 | 51 | let customize = protobuf_codegen::Customize::default() 52 | .gen_mod_rs(true) 53 | .generate_accessors(true); 54 | 55 | protobuf_codegen::Codegen::new() 56 | .pure() 57 | .out_dir(&proto_out_dir) 58 | .include("../../proto") 59 | .inputs(PROTOS) 60 | .customize(customize) 61 | .run().unwrap(); 62 | } 63 | -------------------------------------------------------------------------------- /crates/rrg/src/bin/rrg_oneshot.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | //! Developer command to run a Fleetspeakless one-shot RRG action. 7 | //! 8 | //! e.g. you may run an action as root with: 9 | //! ```text 10 | //! cargo build && (protoc --proto_path=proto/ --encode=rrg.Request proto/rrg.proto proto/rrg/action/*.proto | sudo -A ./target/debug/rrg_oneshot) < Self { 30 | Self { args } 31 | } 32 | } 33 | 34 | impl rrg::session::Session for OneshotSession { 35 | fn args(&self) -> &rrg::args::Args { 36 | &self.args 37 | } 38 | 39 | fn reply(&mut self, item: I) -> rrg::session::Result<()> 40 | where 41 | I: rrg::Item + 'static, 42 | { 43 | println!( 44 | "Reply: {}", 45 | protobuf::text_format::print_to_string_pretty(&item.into_proto()) 46 | ); 47 | Ok(()) 48 | } 49 | 50 | fn send(&mut self, sink: rrg::Sink, item: I) -> rrg::session::Result<()> 51 | where 52 | I: rrg::Item + 'static, 53 | { 54 | println!( 55 | "Sent to {sink:?}: {}", 56 | protobuf::text_format::print_to_string_pretty(&item.into_proto()) 57 | ); 58 | Ok(()) 59 | } 60 | 61 | fn heartbeat(&mut self) {} 62 | } 63 | 64 | fn main() { 65 | let args = rrg::args::from_env_args(); 66 | rrg::log::init(&args); 67 | 68 | // rust-protobuf does not support Any in text or JSON formats, so we're 69 | // stuck taking in encoded protobufs. 70 | // See https://github.com/stepancheg/rust-protobuf/issues/628 71 | let request_proto = rrg_proto::rrg::Request::parse_from_reader(&mut std::io::stdin()) 72 | .expect("Failed to parse request protobuf"); 73 | let request = rrg::Request::try_from(request_proto).expect("Failed to parse request"); 74 | let mut session = OneshotSession::with_args(args); 75 | rrg::action::dispatch(&mut session, request).unwrap(); 76 | } 77 | -------------------------------------------------------------------------------- /proto/rrg/action/scan_memory_yara.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | import "google/protobuf/duration.proto"; 8 | 9 | package rrg.action.scan_memory_yara; 10 | 11 | message Args { 12 | // PIDs of the processes whose memory we are interested in. 13 | repeated uint32 pids = 1; 14 | 15 | // YARA signature source to use for scanning. 16 | string signature = 2; 17 | 18 | // Maximum time spent scanning a single process. 19 | google.protobuf.Duration timeout = 3; 20 | 21 | // Set this flag to avoid scanning mapped files. 22 | bool skip_mapped_files = 4; 23 | // Set this flag to avoid scanning shared memory regions. Applies to Linux only. 24 | bool skip_shared_regions = 5; 25 | // Set this flag to avoid scanning regions marked as executable. 26 | bool skip_executable_regions = 6; 27 | // Set this flag to avoid scanning regions marked as readable and not writable or executable. 28 | bool skip_readonly_regions = 7; 29 | 30 | // Length of the chunks used to read large memory regions, in bytes. 31 | // Will use a reasonable default value if unset. 32 | optional uint64 chunk_size = 8; 33 | // Overlap across chunks, in bytes. A larger overlap decreases 34 | // the chance of missing a string that would otherwise match, 35 | // but is located across chunk boundaries. 36 | // Will use a reasonable default value if unset. 37 | optional uint64 chunk_overlap = 9; 38 | } 39 | 40 | message Match { 41 | // Offset of the matching string into the process' address space. 42 | uint64 offset = 2; 43 | // A SHA-256 hash of the blob of matching data, which was sent to the blob sink. 44 | bytes data_sha256 = 4; 45 | } 46 | 47 | message Pattern { 48 | // The name of this pattern. 49 | string identifier = 1; 50 | // Matching occurrences of this pattern. 51 | repeated Match matches = 2; 52 | } 53 | 54 | message Rule { 55 | // The name of this rule. 56 | string identifier = 1; 57 | // Patterns which this rule searches for. 58 | repeated Pattern patterns = 2; 59 | } 60 | 61 | // Result of scanning memory for one single process. 62 | message Result { 63 | // PID of the process this result refers to. 64 | uint32 pid = 1; 65 | 66 | // Yara rules which matched when scanning this process, if any. 67 | // Only set if `error` is unset. 68 | repeated Rule matching_rules = 2; 69 | 70 | // Error message set if something went wrong when scanning this process' memory. 71 | string error = 9; 72 | } 73 | -------------------------------------------------------------------------------- /crates/ospect/src/proc/linux.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | /// Returns an iterator yielding identifiers of all processes on the system. 7 | pub fn ids() -> std::io::Result>> { 8 | Ids::new() 9 | } 10 | 11 | /// A Linux-specific implementation of the iterator over process identifiers. 12 | struct Ids { 13 | /// An iterator over contents of the `/proc` directory. 14 | iter: std::fs::ReadDir, 15 | } 16 | 17 | impl Ids { 18 | 19 | /// Creates a new iterator over system process identifiers. 20 | fn new() -> std::io::Result { 21 | let iter = std::fs::read_dir("/proc")?; 22 | Ok(Ids { iter }) 23 | } 24 | } 25 | 26 | impl Iterator for Ids { 27 | type Item = std::io::Result; 28 | 29 | fn next(&mut self) -> Option> { 30 | use std::str::FromStr as _; 31 | 32 | for entry in &mut self.iter { 33 | let entry = match entry { 34 | Ok(entry) => entry, 35 | Err(error) => return Some(Err(error)), 36 | }; 37 | 38 | // Processes are represented by directories, so we should skip all 39 | // that are not to skip unnecessary parsing. Note that according to 40 | // the documentation on most Unix platforms this function should not 41 | // make any additional calls to the operating system, so this check 42 | // is cheap. 43 | match entry.file_type() { 44 | Ok(file_type) if file_type.is_dir() => (), 45 | _ => continue, 46 | } 47 | 48 | // Because we are interested only in file names that are integers, 49 | // we can safely discard any that are not valid Unicode names (which 50 | // should not be the case in general, as all names within the procfs 51 | // should only use ASCII). 52 | let file_name = entry.file_name(); 53 | let file_name_str = match file_name.to_str() { 54 | Some(file_name_str) => file_name_str, 55 | None => continue, 56 | }; 57 | 58 | // All directories under `/proc` that are integers should correspond 59 | // to a process, so they are valid pids. Everything else we can just 60 | // discard. 61 | let pid = match u32::from_str(file_name_str) { 62 | Ok(pid) => pid, 63 | Err(_) => continue, 64 | }; 65 | 66 | return Some(Ok(pid)); 67 | } 68 | 69 | None 70 | 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/rrg/src/action/list_connections.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | use log::warn; 7 | 8 | /// A result of the `list_connections` action. 9 | struct Item { 10 | // Information about the individual connection. 11 | conn: ospect::net::Connection, 12 | } 13 | 14 | // Handles invocations of the `list_connections` action. 15 | pub fn handle(session: &mut S, _: ()) -> crate::session::Result<()> 16 | where 17 | S: crate::session::Session, 18 | { 19 | let conns = ospect::net::all_connections() 20 | .map_err(crate::session::Error::action)?; 21 | 22 | for conn in conns { 23 | let conn = match conn { 24 | Ok(conn) => conn, 25 | Err(error) => { 26 | warn!("failed to obtain connection information: {}", error); 27 | continue; 28 | } 29 | }; 30 | 31 | session.reply(Item { 32 | conn, 33 | })?; 34 | } 35 | 36 | Ok(()) 37 | } 38 | 39 | impl crate::response::Item for Item { 40 | 41 | type Proto = rrg_proto::list_connections::Result; 42 | 43 | fn into_proto(self) -> rrg_proto::list_connections::Result { 44 | let mut proto = rrg_proto::list_connections::Result::new(); 45 | proto.set_connection(self.conn.into()); 46 | 47 | proto 48 | } 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | 54 | use super::*; 55 | 56 | #[test] 57 | fn handle_local_tcp_connection() { 58 | use std::net::Ipv4Addr; 59 | 60 | let server = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)) 61 | .unwrap(); 62 | let server_addr = server.local_addr() 63 | .unwrap(); 64 | 65 | let mut session = crate::session::FakeSession::new(); 66 | assert!(handle(&mut session, ()).is_ok()); 67 | 68 | let item = session.replies::().find(|item| { 69 | item.conn.local_addr() == server_addr 70 | }).unwrap(); 71 | 72 | if let ospect::net::Connection::Tcp(conn) = item.conn { 73 | assert_eq!(conn.state(), ospect::net::TcpState::Listen); 74 | } else { 75 | panic!(); 76 | } 77 | } 78 | 79 | #[test] 80 | fn handle_local_udp_connection() { 81 | use std::net::Ipv4Addr; 82 | 83 | let socket = std::net::UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)) 84 | .unwrap(); 85 | let socket_addr = socket.local_addr() 86 | .unwrap(); 87 | 88 | let mut session = crate::session::FakeSession::new(); 89 | assert!(handle(&mut session, ()).is_ok()); 90 | 91 | let item = session.replies::().find(|item| { 92 | item.conn.local_addr() == socket_addr 93 | }).unwrap(); 94 | 95 | assert!(matches!(item.conn, ospect::net::Connection::Udp(_))); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RRG 2 | === 3 | 4 | [![CI status][ci-badge]][ci] 5 | 6 | RRG is a *[Rust][rust] rewrite of [GRR][grr]* (a remote live forensics 7 | framework). 8 | 9 | It strives to evaluate how feasible it is to rewrite the client-side part of GRR 10 | (an agent service) without all the historical baggage that the current version 11 | has to carry. For example, it does not implement its own communication layer, 12 | but leverages [Fleetspeak][fleetspeak] for that. It also tries to assess how 13 | many existing issues related to the Python codebase could be resolved by using a 14 | modern language with powerful type system and strong safety guarantees. 15 | 16 | This project is not an official Google product, is under heavy development and 17 | should not be used for any production deployments. So far, it is nothing more 18 | than an experiment. 19 | 20 | [rust]: https://rust-lang.org 21 | [grr]: https://github.com/google/grr 22 | [fleetspeak]: https://github.com/google/fleetspeak 23 | 24 | [ci]: https://github.com/google/rrg/actions?query=workflow%3AIntegrate 25 | [ci-badge]: https://github.com/google/rrg/workflows/Integrate/badge.svg 26 | 27 | Development 28 | ----------- 29 | 30 | ### Prerequisites 31 | 32 | RRG is written in Rust and needs a Rust toolchain to be built. The recommended 33 | way of installing Rust is to use [rustup](https://rustup.rs/). 34 | 35 | Because RRG is only a component of a bigger system, to do anything useful with 36 | it you also need to [setup Fleetspeak][fleetspeak-guide] and [GRR][grr-guide]. 37 | 38 | [fleetspeak-guide]: https://github.com/google/fleetspeak/blob/master/docs/guide.md 39 | [grr-guide]: https://grr-doc.readthedocs.io/en/latest/fleetspeak/from-source.html 40 | 41 | ### Building 42 | 43 | RRG uses Cargo for everything, so building it is as easy as running: 44 | 45 | $ cargo build 46 | 47 | This will create a unoptimized executable `target/debug/rrg`. 48 | 49 | To create release executable (note that this is much slower and is not suitable 50 | for quick iterations) run: 51 | 52 | $ cargo build --release 53 | 54 | This will create an optimized executable `target/release/rrg`. 55 | 56 | ### Testing 57 | 58 | To run all tests: 59 | 60 | $ cargo test 61 | 62 | To run tests only for a particular crate: 63 | 64 | $ cargo test --package='ospect' 65 | 66 | To run only a particular test: 67 | 68 | $ cargo test --package='rrg' action::get_file_contents::tests::handle_empty_file 69 | 70 | To verify that the code compiles on all supported platforms: 71 | 72 | $ cargo check --tests --target='x86_64-unknown-linux-gnu' --target='x86_64-apple-darwin' --target='x86_64-pc-windows-gnu' --target='aarch64-unknown-linux-gnu' 73 | 74 | Note that this requires additional toolchains for cross-compilation to be 75 | [installed](https://rust-lang.github.io/rustup/cross-compilation.html). 76 | 77 | It is also possible to use cross-compilation and tools like Wine to run tests 78 | on another operating system: 79 | 80 | $ cargo test --target='x86_64-pc-windows-gnu' --package='rrg' --no-run 81 | $ wine target/x86_64-pc-windows-gnu/debug/deps/rrg-bcf99adf861ea84a.exe 82 | 83 | Structure 84 | --------- 85 | 86 | ### Directories 87 | 88 | * `crates/` — All Rust crates that the project consists of live here. 89 | * `docs/` — All non-code documentation and guides live here. 90 | * `proto/` — All Protocol Buffers definitions describing RRG's API live here. 91 | 92 | ### Crates 93 | 94 | * `ospect` — Tools for inspecting the operating system. 95 | * `rrg` — Implementation of all agent actions and the entry point. 96 | * `rrg-proto` — Code generated from Protocol Buffer definitions. 97 | -------------------------------------------------------------------------------- /proto/rrg/net.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.net; 8 | 9 | // IP address (either IPv4 or IPv6). 10 | message IpAddress { 11 | // Octets that the IP address consists of. 12 | // 13 | // Required to have 4 bytes for IPv4 and 16 bytes for IPv6 addresses. 14 | bytes octets = 1; 15 | } 16 | 17 | // Socket address (either IPv4 or IPv6). 18 | message SocketAddress { 19 | // IP address associated with this socket address. 20 | IpAddress ip_address = 1; 21 | 22 | // Port number associated with this socket address. 23 | uint32 port = 2; 24 | } 25 | 26 | // MAC address as defined in the IEEE 802 standard [1]. 27 | // 28 | // [1]: https://standards.ieee.org/wp-content/uploads/import/documents/tutorials/macgrp.pdf 29 | message MacAddress { 30 | // Octets that the MAC address consists of. 31 | // 32 | // Required to have 6 bytes. 33 | bytes octets = 1; 34 | } 35 | 36 | // State of a TCP connection as described in RFC 793 [1]. 37 | // 38 | // [1]: https://www.ietf.org/rfc/rfc793.txt 39 | enum TcpState { 40 | UNKNOWN = 0x00; 41 | ESTABLISHED = 0x01; 42 | SYN_SENT = 0x02; 43 | SYN_RECEIVED = 0x03; 44 | FIN_WAIT_1 = 0x04; 45 | FIN_WAIT_2 = 0x05; 46 | TIME_WAIT = 0x06; 47 | CLOSED = 0x07; 48 | CLOSE_WAIT = 0x08; 49 | LAST_ACK = 0x09; 50 | LISTEN = 0x0A; 51 | CLOSING = 0x0B; 52 | } 53 | 54 | // Information about a TCP connection. 55 | // 56 | // The version of the protocol can be determined from the IP addresses. 57 | message TcpConnection { 58 | // Identifier of the process that owns the connection. 59 | uint32 pid = 1; 60 | 61 | // Local address of the connection. 62 | SocketAddress local_address = 2; 63 | 64 | // Remote address of the connection. 65 | SocketAddress remote_address = 3; 66 | 67 | // State of the connection. 68 | TcpState state = 4; 69 | } 70 | 71 | // Information about a UDP connection. 72 | // 73 | // The version of the protocol can be determined from the IP addresses. 74 | message UdpConnection { 75 | // Identifier of the process that owns the connection. 76 | uint32 pid = 1; 77 | 78 | // Local address of the connection. 79 | SocketAddress local_address = 2; 80 | } 81 | 82 | // Information about an Internet connection. 83 | // 84 | // The version of the protocol can be determined from the IP addresses. 85 | message Connection { 86 | oneof connection { 87 | // Information about a TCP connection. 88 | TcpConnection tcp = 1; 89 | 90 | // Information about a UDP connection. 91 | UdpConnection udp = 2; 92 | } 93 | } 94 | 95 | // Information about a network interface. 96 | message Interface { 97 | // A name of the interface as reported by the system. 98 | // 99 | // Note that on some system (e.g. Linux), the interface may consist of pretty 100 | // much arbitrary bytes and might not be compatible with Unicode. Because this 101 | // is not very probable and ergonomics of using a raw `bytes` field, invalid 102 | // bytes are going to be subsituted with the replacement character ("�"). 103 | string name = 1; 104 | 105 | // MAC address associated with the interface. 106 | MacAddress mac_address = 2; 107 | 108 | // IP addresses associated with the interface. 109 | repeated IpAddress ip_addresses = 3; 110 | 111 | // A friendly name of the interface as reported by the system. 112 | // 113 | // Windows-only. 114 | string windows_friendly_name = 4; 115 | } 116 | -------------------------------------------------------------------------------- /crates/ospect/src/proc/windows.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | /// Returns an iterator yielding identifiers of all processes on the system. 7 | pub fn ids() -> std::io::Result>> { 8 | Ids::new() 9 | } 10 | 11 | /// A Windows-specific implementation of the iterator over process identifiers. 12 | struct Ids { 13 | /// An iterator over the process identifiers returned by `EnumProcesses`. 14 | iter: std::vec::IntoIter, 15 | } 16 | 17 | impl Ids { 18 | 19 | /// Creates a new iterator over system process identifiers. 20 | fn new() -> std::io::Result { 21 | use windows_sys::Win32::Foundation::*; 22 | 23 | let mut buf_cap = DEFAULT_PID_BUF_CAP; 24 | 25 | loop { 26 | let mut buf = Vec::with_capacity(buf_cap); 27 | let mut buf_size = std::mem::MaybeUninit::uninit(); 28 | 29 | // SAFETY: We allocate the buffer above and pass its size (capacity 30 | // multiplied by the size of individual element). In case the buffer 31 | // is too small, the function should return an appropriate error. 32 | let status = unsafe { 33 | windows_sys::Win32::System::ProcessStatus::K32EnumProcesses( 34 | buf.as_mut_ptr(), 35 | (buf_cap * std::mem::size_of::()) as u32, 36 | buf_size.as_mut_ptr(), 37 | ) 38 | }; 39 | 40 | if status == FALSE { 41 | // SAFETY: We are on Windows and the function should be safe to 42 | // call in all context. 43 | let code = unsafe { GetLastError() }; 44 | 45 | // If the provided buffer is not big enough, we try again we a 46 | // one that is twice as big until we reach the limit and bail 47 | // out (which is handled be the error handler below). 48 | if code == ERROR_INSUFFICIENT_BUFFER { 49 | buf_cap *= 2; 50 | if buf_cap <= MAX_PID_BUF_CAP { 51 | continue; 52 | } 53 | } 54 | 55 | return Err(std::io::Error::from_raw_os_error(code as i32)); 56 | } 57 | 58 | // SAFETY: The call to `EnumProcesses` succeeded, so the `buf_size` 59 | // variable should contain the number of bytes filled in the buffer. 60 | let buf_size = unsafe { buf_size.assume_init() } as usize; 61 | 62 | if buf_size % std::mem::size_of::() != 0 { 63 | return Err(std::io::ErrorKind::InvalidData.into()); 64 | } 65 | let buf_len = buf_size / std::mem::size_of::(); 66 | 67 | // SAFETY: Since `buf_size` contains the amount of bytes filled in 68 | // the buffer we can now safely set the length of the buffer. 69 | unsafe { 70 | buf.set_len(buf_len); 71 | } 72 | 73 | return Ok(Ids { 74 | iter: buf.into_iter() 75 | }); 76 | } 77 | } 78 | } 79 | 80 | impl Iterator for Ids { 81 | 82 | type Item = std::io::Result; 83 | 84 | fn next(&mut self) -> Option> { 85 | self.iter.next().map(Ok) 86 | } 87 | } 88 | 89 | /// The default capacity of the process identifiers buffer. 90 | const DEFAULT_PID_BUF_CAP: usize = 1024; 91 | 92 | /// The maximum capacity of the process identifiers buffer. 93 | const MAX_PID_BUF_CAP: usize = 16384; 94 | -------------------------------------------------------------------------------- /proto/rrg/action/dump_process_memory.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.dump_process_memory; 8 | 9 | import "rrg/fs.proto"; 10 | 11 | message Args { 12 | // PIDs of the processes whose memory we are interested in. 13 | repeated uint32 pids = 1; 14 | 15 | // Maximum amount of process memory to dump. Applies across all processes 16 | // specified in `pids`. The first memory region that exceeds the limit 17 | // will be dumped partially. 18 | optional uint64 total_size_limit = 2; 19 | 20 | // Memory offsets to prioritize when the process memory size is greater than 21 | // the limit specified in `total_size_limit`. First, memory pages containing 22 | // the offsets will be dumped up to size_limit. If not reached, the 23 | // remaining memory pages will be dumped up to size_limit. 24 | repeated uint64 priority_offsets = 3; 25 | 26 | // Set this flag to avoid dumping mapped files. 27 | bool skip_mapped_files = 4; 28 | // Set this flag to avoid dumping shared memory regions. Applies to Linux only. 29 | bool skip_shared_regions = 5; 30 | // Set this flag to avoid dumping regions marked as executable. 31 | bool skip_executable_regions = 6; 32 | // Set this flag to avoid dumping regions marked as readable and not writable or executable. 33 | bool skip_readonly_regions = 7; 34 | } 35 | 36 | // Set of OS-level permissions associated with a memory region. 37 | message Permissions { 38 | // Indicates the region of memory can be read. 39 | bool read = 1; 40 | // Indicates the region of memory can be written to. 41 | bool write = 2; 42 | // Indicates the region of memory contains executable data. 43 | bool execute = 3; 44 | // Indicates a region of memory that was mapped in shared mode. 45 | // Applies to Linux only. 46 | bool shared = 4; 47 | // Indicates a region of memory that was mapped in private mode. 48 | bool private = 5; 49 | } 50 | 51 | // The result of the dump_process_memory action. 52 | // Represents one chunk of a single memory region of a running process. 53 | // The chunk can be smaller than the memory region if the size of the region 54 | // exceeds the maximum blob size of the blob sink. In that case, multiple 55 | // `Result`s will be returned, each with a different `offset` and `size`. 56 | message Result { 57 | // PID of the process this region belongs to. 58 | uint32 pid = 1; 59 | 60 | // Start offset of the memory region in the process' address space. 61 | uint64 region_start = 2; 62 | // End offset of the memory region in the process' address space. 63 | uint64 region_end = 3; 64 | 65 | // A SHA-256 hash of the chunk of memory contents sent to the blob sink. 66 | // 67 | // Set only if `error` is not set. 68 | bytes blob_sha256 = 4; 69 | 70 | // Offset relative to `region_start` that this chunk starts at. 71 | // 72 | // Set only if `error` is not set. 73 | uint64 offset = 5; 74 | 75 | // Size of the chunk of memory that was sent to the blob sink. 76 | // Will be <= `region_end - region_start` 77 | // 78 | // Set only if `error` is not set. 79 | uint64 size = 6; 80 | 81 | // Permissions associated with this region of memory. 82 | // 83 | // Set only if `error` is not set. 84 | Permissions permissions = 7; 85 | 86 | // Set if this region of memory is mapped to a file on disk. 87 | // Contains the absolute path to the file in question. 88 | // 89 | // Set only if `error` is not set. 90 | rrg.fs.Path file_path = 8; 91 | 92 | // Error message set if something went wrong when processing the region. 93 | string error = 9; 94 | } 95 | -------------------------------------------------------------------------------- /proto/rrg/action/get_filesystem_timeline.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.get_filesystem_timeline; 8 | 9 | import "rrg/fs.proto"; 10 | 11 | message Args { 12 | // Absolute path to the root directory to get the timeline of. 13 | rrg.fs.Path root = 1; 14 | } 15 | 16 | message Result { 17 | // A SHA-256 hash of the timeline batch sent to the blob sink. 18 | // 19 | // Because the entire timeline can easily have millions of entries, it could 20 | // quickly exceed the maximum allowed size for a message. This is why entries 21 | // are batched, gzipped and then send as blobs to the blobstore. 22 | bytes blob_sha256 = 1; 23 | 24 | // The total number of entries in the chunk. 25 | // 26 | // This number includes only entries contained in the chunk corresponding to 27 | // this result, not the total number of entries the action execution processed 28 | // so far. 29 | uint64 entry_count = 2; 30 | } 31 | 32 | // An individual entry of the timeline. 33 | // 34 | // Note that this type does not use wrappers such as `rrg.fs.FileMetadata` or 35 | // `google.protobuf.Timestamp`. There are two reasons for this: performance and 36 | // compatibility. 37 | // 38 | // To avoid unnecessary nesting that has performance implications (both in terms 39 | // of CPU, memory and network utilization) this message is allowed to have only 40 | // primitive fields. 41 | // 42 | // Moreover, because timeline is stored in binary form we should consider it to 43 | // be a file format on its own. This, this structure should be in-sync with what 44 | // GRR currently uses to represent timeline [1]. 45 | // 46 | // The message itself is based on the POSIX definition of stat [2] and is typed 47 | // according to the POSIX standard [3]. 48 | // 49 | // [1]: https://github.com/google/grr/blob/cab3a1fe590a72862ed42ef92a6a57fd831a5783/grr/proto/grr_response_proto/timeline.proto#L49-L91 50 | // [2]: http://pubs.opengroup.org/onlinepubs/009695399/basedefs/sys/stat.h.html 51 | // [3]: http://pubs.opengroup.org/onlinepubs/009695399/basedefs/sys/types.h.html 52 | message Entry { 53 | // An absolute path to the file this entry corresponds to. 54 | // 55 | // This field uses the same path encoding as the `raw_bytes` field of the 56 | // `rrg.fs.Path` message. 57 | optional bytes path = 1; 58 | 59 | // Mode of the file defined as standard POSIX bitmask. 60 | // 61 | // Unix-only. 62 | optional int64 unix_mode = 2; 63 | 64 | // Size of the file in bytes. 65 | optional uint64 size = 3; 66 | 67 | // Identifier of the device containing the file. 68 | // 69 | // Unix-only. 70 | optional int64 unix_dev = 4; 71 | 72 | // Serial number of the file. 73 | // 74 | // This field is set only on Unix-like systems. 75 | optional uint64 unix_ino = 5; 76 | 77 | // Identifier of the user owning the file. 78 | // 79 | // Unix-only. 80 | optional int64 unix_uid = 6; 81 | 82 | // Identifier of the group owning the file. 83 | // 84 | // Unix-only. 85 | optional int64 unix_gid = 7; 86 | 87 | // Time of the last access of the file in nanoseconds since epoch. 88 | optional int64 atime_nanos = 8; 89 | 90 | // Time of the last data change of the file in nanoseconds since epoch. 91 | optional int64 mtime_nanos = 9; 92 | 93 | // Time of the last status change of the file in nanoseconds since epoch. 94 | optional int64 ctime_nanos = 10; 95 | 96 | // Time of the file creation in nanoseconds since epoch. 97 | optional int64 btime_nanos = 11; 98 | 99 | // Extra file attributes. 100 | // 101 | // Windows-only. 102 | optional uint64 windows_attributes = 12; 103 | } 104 | -------------------------------------------------------------------------------- /proto/rrg/fs.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.fs; 8 | 9 | import "google/protobuf/timestamp.proto"; 10 | 11 | // Path in the filesystem. 12 | message Path { 13 | // Raw bytes representing the path. 14 | // 15 | // Different operating systems represent paths differently and have varying 16 | // capabilities. In most general case, Linux systems allow paths with pretty 17 | // much arbitrary bytes and so we need to support this case. 18 | // 19 | // On Windows, where paths are stored using UCS-2 encoding (that is 16-bit), 20 | // represent paths with the WTF-8 encoding [1]: an "almost UTF-8". So, even 21 | // if the language does not have support for WTF-8, we can treat it as UTF-8 22 | // and still get more-or-less meaningful results. 23 | // 24 | // [1]: https://simonsapin.github.io/wtf-8 25 | bytes raw_bytes = 1; 26 | } 27 | 28 | // Metadata associated with a specific file. 29 | message FileMetadata { 30 | // List of different file types. 31 | enum Type { 32 | // Unknown (or unspecified). 33 | UNKNOWN = 0; 34 | // Regular file. 35 | FILE = 1; 36 | // Directory. 37 | DIR = 2; 38 | // Symbolic link. 39 | SYMLINK = 3; 40 | } 41 | 42 | // Type of the file. 43 | Type type = 1; 44 | // Size of the file in bytes. 45 | uint64 size = 2; 46 | // Time at which the file was last accessed. 47 | google.protobuf.Timestamp access_time = 3; 48 | // Time at which the file was last modified. 49 | google.protobuf.Timestamp modification_time = 4; 50 | // Time at which the file was created. 51 | google.protobuf.Timestamp creation_time = 5; 52 | 53 | // Identifier of the device containing the file (Unix-only). 54 | uint64 unix_dev = 6; 55 | // Inode number of the file (Unix-only). 56 | uint64 unix_ino = 7; 57 | // Type and rights mask of the file (Unix-only). 58 | uint32 unix_mode = 8; 59 | // Number of hard links pointing to the file (Unix-only). 60 | uint64 unix_nlink = 9; 61 | // Identifier of the user owning the file (Unix-only). 62 | uint32 unix_uid = 10; 63 | // Identifier of the group owning the file (Unix-only). 64 | uint32 unix_gid = 11; 65 | // Identifier of the device (only for special files, Unix-only). 66 | uint64 unix_rdev = 12; 67 | // Block size for the filesystem I/O. 68 | uint64 unix_blksize = 13; 69 | // Number of blocks allocated for the file. 70 | uint64 unix_blocks = 14; 71 | } 72 | 73 | // Extended attribute of a file. 74 | // 75 | // Note that extended attributes are not available on Windows and there are some 76 | // differences between how they work on macOS and Linux. See the [Wikipedia][1] 77 | // article for more details. 78 | // 79 | // [Wikipedia]: https://en.wikipedia.org/wiki/Extended_file_attributes 80 | message FileExtAttr { 81 | // A name of the article. 82 | // 83 | // On macOS this is an UTF-8 encoded string, but on Linux it can consist of 84 | // arbitrary byte sequence (although most probably it will be also an UTF-8 85 | // string). 86 | bytes name = 1; 87 | // A value of the attribute. 88 | // 89 | // This can be an arbitrary sequence of bytes both on macOS and Linux. 90 | bytes value = 2; 91 | } 92 | 93 | // Information about a mounted filesystem. 94 | message Mount { 95 | // Name or other identifier of the mounted device. 96 | string name = 1; 97 | // Path at which the mounted filesystem is available (a mount point). 98 | Path path = 2; 99 | // Type of the mounted filesystem (e.g. `ext4`, `ramfs`, `NTFS`). 100 | string fs_type = 3; 101 | } 102 | -------------------------------------------------------------------------------- /crates/rrg/src/action/get_system_metadata.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | use log::error; 6 | 7 | /// A result of the the `get_system_metadata` action. 8 | struct Item { 9 | /// The kind of the operating system the agent is running on. 10 | kind: ospect::os::Kind, 11 | /// Version string of the operating system the agent is running on. 12 | version: Option, 13 | /// CPU architecture of the operating system the agent is running on. 14 | arch: Option, 15 | /// Hostname of the operating system the agent is running on. 16 | hostname: Option, 17 | /// FQDN of the operating system the agent is running on. 18 | fqdn: Option, 19 | /// Estimated time at which the operating system was installed. 20 | installed: Option, 21 | } 22 | 23 | impl Item { 24 | 25 | /// Returns metadata of the operating system the agent is running on. 26 | fn new() -> std::io::Result { 27 | let version = match ospect::os::version() { 28 | Ok(version) => Some(version), 29 | Err(error) => { 30 | error!("failed to collect system version: {error}"); 31 | None 32 | } 33 | }; 34 | let arch = match ospect::os::arch() { 35 | Ok(arch) => Some(arch), 36 | Err(error) => { 37 | error!("failed to collect system architecture: {error}"); 38 | None 39 | } 40 | }; 41 | let hostname = match ospect::os::hostname() { 42 | Ok(hostname) => Some(hostname), 43 | Err(error) => { 44 | error!("failed to collect system hostname: {error}"); 45 | None 46 | } 47 | }; 48 | let fqdn = match ospect::os::fqdn() { 49 | Ok(fqdn) => Some(fqdn), 50 | Err(error) => { 51 | error!("failed to collect system FQDN: {error}"); 52 | None 53 | } 54 | }; 55 | let installed = match ospect::os::installed() { 56 | Ok(installed) => Some(installed), 57 | Err(error) => { 58 | error!("failed to collect system installation time: {error}"); 59 | None 60 | } 61 | }; 62 | 63 | Ok(Item { 64 | kind: ospect::os::kind(), 65 | version, 66 | arch, 67 | hostname, 68 | fqdn, 69 | installed, 70 | }) 71 | } 72 | } 73 | 74 | impl crate::response::Item for Item { 75 | 76 | type Proto = rrg_proto::get_system_metadata::Result; 77 | 78 | fn into_proto(self) -> rrg_proto::get_system_metadata::Result { 79 | use rrg_proto::into_timestamp; 80 | 81 | let mut proto = rrg_proto::get_system_metadata::Result::new(); 82 | proto.set_type(self.kind.into()); 83 | if let Some(version) = self.version { 84 | proto.set_version(version); 85 | } 86 | if let Some(arch) = self.arch { 87 | proto.set_arch(arch); 88 | } 89 | if let Some(hostname) = self.hostname { 90 | proto.set_hostname(hostname.to_string_lossy().into_owned()); 91 | } 92 | if let Some(fqdn) = self.fqdn { 93 | proto.set_fqdn(fqdn.to_string_lossy().into_owned()); 94 | } 95 | if let Some(installed) = self.installed { 96 | proto.set_install_time(into_timestamp(installed)); 97 | } 98 | 99 | proto 100 | } 101 | } 102 | 103 | // Handles invocations of the `get_system_metadata` action. 104 | pub fn handle(session: &mut S, _: ()) -> crate::session::Result<()> 105 | where 106 | S: crate::session::Session, 107 | { 108 | let item = Item::new() 109 | .map_err(crate::session::Error::action)?; 110 | 111 | session.reply(item)?; 112 | 113 | Ok(()) 114 | } 115 | -------------------------------------------------------------------------------- /crates/rrg-proto/src/path/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | #[cfg(any(target_os = "windows", test))] 7 | mod wtf8; 8 | 9 | use std::path::PathBuf; 10 | 11 | /// Interprets given bytes as an operating system path. 12 | /// 13 | /// On Linux, the path is constructed from bytes as is since system paths can 14 | /// be made of arbitrary sequence of bytes. 15 | /// 16 | /// On Windows, the [WTF-8][wtf8] encoding is used. 17 | /// 18 | /// [wtf8]: https://simonsapin.github.io/wtf-8 19 | /// 20 | /// # Examples 21 | /// 22 | /// ``` 23 | /// use std::ffi::OsStr; 24 | /// 25 | /// let path = rrg_proto::path::from_bytes(b"foo/bar/baz".to_vec()).unwrap(); 26 | /// 27 | /// let mut components = path.components() 28 | /// .map(|component| component.as_os_str()); 29 | /// 30 | /// assert_eq!(components.next(), Some(OsStr::new("foo"))); 31 | /// assert_eq!(components.next(), Some(OsStr::new("bar"))); 32 | /// assert_eq!(components.next(), Some(OsStr::new("baz"))); 33 | /// assert_eq!(components.next(), None); 34 | /// ``` 35 | pub fn from_bytes(bytes: Vec) -> Result { 36 | from_bytes_impl(bytes) 37 | } 38 | 39 | /// Serializes given path to a byte sequence. 40 | /// 41 | /// On Linux, the path is emitted as is since system paths can consist of 42 | /// arbitrary bytes. 43 | /// 44 | /// On Windows, the [WTF-8][wtf8] encoding is used. 45 | /// 46 | /// [wtf8]: https://simonsapin.github.io/wtf-8 47 | /// 48 | /// # Examples 49 | /// 50 | /// ``` 51 | /// use std::path::PathBuf; 52 | /// 53 | /// let path = PathBuf::from("foo/bar/baz"); 54 | /// assert_eq!(rrg_proto::path::into_bytes(path), b"foo/bar/baz"); 55 | /// ``` 56 | pub fn into_bytes(path: PathBuf) -> Vec { 57 | into_bytes_impl(path) 58 | } 59 | 60 | #[cfg(target_family = "unix")] 61 | fn from_bytes_impl(bytes: Vec) -> Result { 62 | use std::os::unix::ffi::OsStringExt as _; 63 | Ok(std::ffi::OsString::from_vec(bytes).into()) 64 | } 65 | 66 | #[cfg(target_family = "windows")] 67 | fn from_bytes_impl(bytes: Vec) -> Result { 68 | let bytes_u16 = wtf8::into_ill_formed_utf16(bytes)?; 69 | 70 | use std::os::windows::ffi::OsStringExt as _; 71 | Ok(std::ffi::OsString::from_wide(&bytes_u16).into()) 72 | } 73 | 74 | #[cfg(target_family = "unix")] 75 | fn into_bytes_impl(path: PathBuf) -> Vec { 76 | use std::os::unix::ffi::OsStringExt as _; 77 | std::ffi::OsString::from(path).into_vec() 78 | } 79 | 80 | #[cfg(target_family = "windows")] 81 | fn into_bytes_impl(path: PathBuf) -> Vec { 82 | let string = std::ffi::OsString::from(path); 83 | 84 | use std::os::windows::ffi::OsStrExt as _; 85 | wtf8::from_ill_formed_utf16(string.as_os_str().encode_wide()) 86 | } 87 | 88 | /// A type representing errors that can occur when parsing paths. 89 | #[derive(Debug, PartialEq, Eq)] 90 | pub struct ParseError { 91 | /// Detailed information about the error. 92 | kind: ParseErrorKind, 93 | } 94 | 95 | impl std::fmt::Display for ParseError { 96 | 97 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 98 | #[cfg(target_family = "unix")] 99 | let _ = fmt; // Unused. 100 | 101 | match self.kind { 102 | #[cfg(target_family = "windows")] 103 | ParseErrorKind::Wtf8(ref error) => write!(fmt, "{}", error), 104 | } 105 | } 106 | } 107 | 108 | impl std::error::Error for ParseError { 109 | 110 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 111 | match self.kind { 112 | #[cfg(target_family = "windows")] 113 | ParseErrorKind::Wtf8(ref error) => Some(error), 114 | } 115 | } 116 | } 117 | 118 | #[cfg(target_family = "windows")] 119 | impl From for ParseError { 120 | 121 | fn from(error: wtf8::ParseError) -> ParseError { 122 | ParseError { 123 | kind: ParseErrorKind::Wtf8(error), 124 | } 125 | } 126 | } 127 | 128 | /// A type enumerating all possible error kinds of path parsing errors. 129 | #[derive(Debug, PartialEq, Eq)] 130 | #[non_exhaustive] 131 | enum ParseErrorKind { 132 | /// Parsing failed because of issues with decoding a WTF-8 string. 133 | #[cfg(target_family = "windows")] 134 | Wtf8(wtf8::ParseError), 135 | } 136 | -------------------------------------------------------------------------------- /crates/ospect/src/proc/macos.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | /// Returns an iterator yielding identifiers of all processes on the system. 7 | pub fn ids() -> std::io::Result>> { 8 | Ids::new() 9 | } 10 | 11 | /// A macOS-specific implementation of the iterator over process identifiers. 12 | struct Ids { 13 | /// An iterator over the process metadata returned by a `sysctl` call. 14 | iter: std::vec::IntoIter, 15 | } 16 | 17 | impl Ids { 18 | 19 | /// Creates a new iterator over system process identifiers. 20 | fn new() -> std::io::Result { 21 | const KINFO_PROC_SIZE: usize = { 22 | std::mem::size_of::() 23 | }; 24 | 25 | let mut mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_ALL]; 26 | 27 | let mut buf_size = std::mem::MaybeUninit::uninit(); 28 | 29 | // SAFETY: We call the `sysctl` function as described in the FreeBSD 30 | // documentation [1] (macOS's kernel derives from FreeBSD). We check for 31 | // errors afterwards. 32 | // 33 | // This is the first call where we don't pass any buffer and we just 34 | // want to estimate the size of the buffer to hold the data. It should 35 | // be returned thought the fourth (`oldlenp`) argument. 36 | // 37 | // Note that the `namelen` parameter (second argument) is the _length_ 38 | // of the array passed as the `name` argument (not size in bytes), while 39 | // the remaining two expect size in bytes. 40 | // 41 | // [1]: https://man.freebsd.org/cgi/man.cgi?sysctl(3) 42 | let code = unsafe { 43 | libc::sysctl( 44 | mib.as_mut_ptr(), mib.len() as libc::c_uint, 45 | std::ptr::null_mut(), buf_size.as_mut_ptr(), 46 | std::ptr::null_mut(), 0, 47 | ) 48 | }; 49 | if code != 0 { 50 | return Err(std::io::Error::last_os_error()); 51 | } 52 | 53 | // SAFETY: If the call to `sysctl` succeeded, we can assume that the 54 | // `buf_size` no is filled with the expected size of the buffer. 55 | let mut buf_size = unsafe { 56 | buf_size.assume_init() 57 | } as usize; 58 | 59 | let mut buf_len = buf_size / KINFO_PROC_SIZE; 60 | if buf_size % KINFO_PROC_SIZE != 0 { 61 | buf_len += 1; 62 | } 63 | 64 | let mut buf = Vec::::with_capacity(buf_len); 65 | 66 | // SAFETY: We create a buffer of the size specified by the previous call 67 | // to `sysctl`. Note that between the two calls the required buffer size 68 | // might have changed in which case `ENOMEM` ought to be returned. The 69 | // operating system should round up the required buffer size to handle 70 | // such cases. We verify whether the call succeeded below. 71 | // 72 | // The rest is as with the `sysctl` call above (not the comment about 73 | // the length parameters). 74 | let code = unsafe { 75 | libc::sysctl( 76 | mib.as_mut_ptr(), mib.len() as libc::c_uint, 77 | buf.as_mut_ptr().cast::(), &mut buf_size, 78 | std::ptr::null_mut(), 0, 79 | ) 80 | }; 81 | if code != 0 { 82 | return Err(std::io::Error::last_os_error()); 83 | } 84 | 85 | if buf_size % KINFO_PROC_SIZE != 0 { 86 | return Err(std::io::ErrorKind::InvalidData.into()); 87 | } 88 | 89 | let buf_len = buf_size / KINFO_PROC_SIZE; 90 | 91 | // SAFETY: The `syctl` call succeeded and we calculated the length of 92 | // the buffer above. 93 | unsafe { 94 | buf.set_len(buf_len); 95 | } 96 | 97 | Ok(Ids { 98 | iter: buf.into_iter(), 99 | }) 100 | } 101 | } 102 | 103 | impl Iterator for Ids { 104 | 105 | type Item = std::io::Result; 106 | 107 | fn next(&mut self) -> Option> { 108 | let proc = self.iter.next()?; 109 | 110 | let pid = match u32::try_from(proc.kp_proc.p_pid) { 111 | Ok(pid) => pid, 112 | Err(error) => { 113 | use std::io::{Error, ErrorKind}; 114 | return Some(Err(Error::new(ErrorKind::InvalidData, error))); 115 | }, 116 | }; 117 | 118 | Some(Ok(pid)) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /proto/rrg/action/execute_signed_command.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | syntax = "proto3"; 7 | 8 | package rrg.action.execute_signed_command; 9 | 10 | import "google/protobuf/duration.proto"; 11 | import "rrg/fs.proto"; 12 | 13 | message Command { 14 | 15 | message Arg { 16 | oneof arg { 17 | // Fixed argument to pass to the executed command. 18 | string signed = 1; 19 | 20 | // Whether to allow execution of arbitrary argument passed in the request 21 | // without it being pre-signed. 22 | bool unsigned_allowed = 2; 23 | } 24 | } 25 | 26 | // Path to the executable file to execute. 27 | rrg.fs.Path path = 1; 28 | 29 | // Arguments to pass to the command. 30 | repeated string args_signed = 2; // TODO: Deprecate this field. 31 | 32 | // Arguments to pass to the command. 33 | repeated Arg args = 6; 34 | 35 | // Environment in which to invoke the command. 36 | // 37 | // Note that environment variables are not inherited from the RRG process 38 | // and are empty if not specified. 39 | map env_signed = 3; 40 | 41 | // Environment variables that can be specified outside of the command. 42 | repeated string env_unsigned_allowed = 7; 43 | 44 | oneof stdin { 45 | // Fixed standard input to pass to the executed command. 46 | bytes signed_stdin = 4; 47 | 48 | // Whether the command should allow execution with arbitrary 49 | // standard input without it being pre-signed. 50 | bool unsigned_stdin_allowed = 5; 51 | } 52 | } 53 | 54 | // TODO: https://github.com/google/rrg/issues/137 55 | // 56 | // This exists solely to support reading preverified commands from a file. Once 57 | // the mechanism of preverified commands is no longer needed, this should be 58 | // deleted. 59 | message CommandList { 60 | // Serialized `Command` messages. 61 | repeated bytes commands = 1; 62 | } 63 | 64 | message Args { 65 | // Serialized `Command` message to execute. 66 | bytes command = 1; 67 | 68 | // Standard input to pass to the executed command. 69 | // 70 | // For this option to work, the command that has been signed has to allow 71 | // arbitrary standard input by having the `unsigned_stdin_allowed` flag set. 72 | bytes unsigned_stdin = 2; 73 | 74 | // Arguments to pass to the executed command. 75 | // 76 | // i-th argument specified here will be provided to the i-th argument of the 77 | // signed command that has `unsigned_arg_allowed` flag set. 78 | repeated string unsigned_args = 5; 79 | 80 | // Environment variables to invoke the executed command with. 81 | // 82 | // For this option to work, the command that has been signed has to have each 83 | // key from this map be present in `env_unsigned_allowed`. 84 | map unsigned_env = 6; 85 | 86 | // An [Ed25519][1] signature of the command. 87 | // 88 | // [1]: https://en.wikipedia.org/wiki/EdDSA#Ed25519 89 | bytes command_ed25519_signature = 3; 90 | 91 | // Timeout after which command execution is aborted. 92 | // 93 | // If not specified, the command execution is aborted immediately. 94 | google.protobuf.Duration timeout = 4; 95 | } 96 | 97 | message Result { 98 | // [Exit code][1] of the command subprocess. 99 | // 100 | // This is available only if the command execution was not aborted by 101 | // a signal which may happen on Unix systems. 102 | // 103 | // [1]: https://en.wikipedia.org/wiki/Exit_status 104 | int32 exit_code = 1; 105 | 106 | // [Exit signal][1] of the command subprocess. 107 | // 108 | // This is available only if the process was terminated by a signal and 109 | // should happen only on Unix systems. 110 | // 111 | // [1]: https://en.wikipedia.org/wiki/Signal_(IPC)#POSIX_signals 112 | int32 exit_signal = 2; 113 | 114 | // Standard output of the command execution. 115 | // 116 | // Because in general standard output can be arbitrarily long, this will 117 | // be truncated to fit within the message limit. `stdout_truncated` field 118 | // will be set if it is the case. 119 | bytes stdout = 3; 120 | 121 | // Standard error of the command execution. 122 | // 123 | // Because in general standard error can be arbitrarily long, this will 124 | // be truncated to fit within the message limit. `stderr_truncated` field 125 | // will be set if it is the case. 126 | bytes stderr = 4; 127 | 128 | // Set if value of `stdout` had to be truncated. 129 | bool stdout_truncated = 5; 130 | 131 | // Set if value of `stderr` had to be truncated. 132 | bool stderr_truncated = 6; 133 | } 134 | 135 | -------------------------------------------------------------------------------- /proto/rrg/action/get_file_metadata.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | syntax = "proto3"; 6 | 7 | package rrg.action.get_file_metadata; 8 | 9 | import "rrg/fs.proto"; 10 | 11 | message Args { 12 | // Root paths to the files to get the metadata for. 13 | // 14 | // If `max_depth` is non-zero, metadata for subfolders and their contents up 15 | // to that limit are returned as well. 16 | // 17 | // Note that if a path points to a symbolic link, the metadata associated 18 | // with the link itself will be returned, not the metadata of the file that 19 | // the link points to. 20 | repeated rrg.fs.Path paths = 1; 21 | 22 | // Limit on the depth of recursion when visiting subfolders. 23 | // 24 | // The default value (0) means that there is no recursion and only metadata 25 | // about the root path is returned. 26 | uint32 max_depth = 2; 27 | 28 | // Whether to collect [MD5 digest][1] of the file contents. 29 | // 30 | // Supported only if the `action-get_file_metadata-md5` feature is enabled. 31 | // 32 | // [1]: https://en.wikipedia.org/wiki/MD5 33 | bool md5 = 3; 34 | 35 | // Whether to collect [SHA-1 digest][1] of the file contents. 36 | // 37 | // Supported only if the `action-get_file_metadata-sha1` feature is enabled. 38 | // 39 | // [1]: https://en.wikipedia.org/wiki/SHA-1 40 | bool sha1 = 4; 41 | 42 | // Whether to collect [SHA-256 digest][2] of the file contents. 43 | // 44 | // Supported only if the `action-get_file_metadata-sha256` feature is enabled. 45 | // 46 | // [1]: https://en.wikipedia.org/wiki/SHA-2 47 | bool sha256 = 5; 48 | 49 | // Regex to restrict the results only to those with matching paths. 50 | // 51 | // Note that this is not merely doing _filtering_ of the results, it is doing 52 | // _pruning_. The difference is that when doing a recursive walk, path that do 53 | // not match the given regex will be discarded from the results and they will 54 | // not be descended into. 55 | string path_pruning_regex = 6; 56 | 57 | // Whether to collect canonical path to the file. 58 | // 59 | // Path canonicalization can be relatively expensive as it might need to 60 | // resolve multiple symlinks along the way and thus should not be enabled for 61 | // cases where long filesystem traversals are expected. 62 | bool path_canonical = 7; 63 | 64 | // Regex to restrict the results only to those with matching contents. 65 | // 66 | // Note that evaluating this condition involves opening the file and reading 67 | // its contents (entirely in the worst case of not matching the regex). Thus, 68 | // this can be an expensive operation. 69 | // 70 | // File contents are split into overlapping chunks and matching is done per 71 | // chunk. This means that the expected matching substring cannot exceed the 72 | // size of the chunk. 73 | string contents_regex = 8; 74 | } 75 | 76 | message Result { 77 | // Path to the file. 78 | // 79 | // This is the original root path of the file as specified in the arguments, 80 | // possibly with some suffix in case of child files. 81 | rrg.fs.Path path = 1; 82 | 83 | // Metadata of the file. 84 | rrg.fs.FileMetadata metadata = 2; 85 | 86 | // Extended attributes of the file. 87 | // 88 | // This field is supported only on Linux and macOS. 89 | repeated rrg.fs.FileExtAttr ext_attrs = 3; 90 | 91 | // A symlink value of the file. 92 | // 93 | // This field is set only if the file is a symlink. 94 | // 95 | // Note that this path might be relative. Moreover, it is not canonicalized 96 | // in any way and might not even exist (a dangling symlink). 97 | rrg.fs.Path symlink = 4; 98 | 99 | // [MD5 digest][1] of the file contents. 100 | // 101 | // Collected only if the `action-get_file_metadata-md5` feature is enabled 102 | // and `md5` argument was provided. 103 | // 104 | // [1]: https://en.wikipedia.org/wiki/MD5 105 | bytes md5 = 5; 106 | 107 | // [SHA-1 digest][1] of the file contents. 108 | // 109 | // Collected only if the `action-get_file_metadata-sha1` feature is enabled 110 | // and `sha1` argument was provided. 111 | // 112 | // [1]: https://en.wikipedia.org/wiki/SHA-1 113 | bytes sha1 = 6; 114 | 115 | // [SHA-256 digest][1] of the file contents. 116 | // 117 | // Collected only if the `action-get_file_metadata-sha256` feature is enabled 118 | // and `sha256` argument was provided. 119 | // 120 | // [1]: https://en.wikipedia.org/wiki/SHA-2 121 | bytes sha256 = 7; 122 | 123 | // Canonical path to the file. 124 | // 125 | // Collected only if requested via the `path_canonical` argument. 126 | rrg.fs.Path path_canonical = 8; 127 | } 128 | -------------------------------------------------------------------------------- /crates/rrg/src/startup.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | /// Information about the agent startup. 7 | pub struct Startup { 8 | /// Metadata about the agent that has been started. 9 | pub metadata: Metadata, 10 | // Path to the agent's executable that is running. 11 | pub path: Option, 12 | /// Value of command-line arguments that the agent was invoked with. 13 | pub args: Vec, 14 | /// Time at which the agent was started. 15 | pub agent_started: std::time::SystemTime, 16 | // TOOD(@panhania): Add support for the `os_booted` field. 17 | pub os_kind: ospect::os::Kind, 18 | } 19 | 20 | impl Startup { 21 | 22 | /// Creates a startup information as of now. 23 | pub fn now() -> Startup { 24 | let path = std::env::current_exe().and_then(std::fs::canonicalize) 25 | .inspect_err(|error| { 26 | log::error!("failed to obtain agent's path: {error}") 27 | }) 28 | .ok(); 29 | 30 | Startup { 31 | metadata: Metadata::from_cargo(), 32 | path, 33 | args: std::env::args().collect(), 34 | agent_started: std::time::SystemTime::now(), 35 | os_kind: ospect::os::kind(), 36 | } 37 | } 38 | } 39 | 40 | impl crate::response::Item for Startup { 41 | type Proto = rrg_proto::startup::Startup; 42 | 43 | fn into_proto(self) -> rrg_proto::startup::Startup { 44 | self.into() 45 | } 46 | } 47 | 48 | /// A type that holds metadata about the RRG agent. 49 | pub struct Metadata { 50 | /// Name of the RRG agent. 51 | pub name: String, 52 | /// Version of the RRG agent. 53 | pub version: Version, 54 | } 55 | 56 | impl Metadata { 57 | 58 | /// Constructs metadata object from Cargo information. 59 | /// 60 | /// This function assumes that are relevant crate information is correctly 61 | /// specified in the `Cargo.toml` file. 62 | pub fn from_cargo() -> Metadata { 63 | Metadata { 64 | name: String::from(env!("CARGO_PKG_NAME")), 65 | version: Version::from_cargo(), 66 | } 67 | } 68 | } 69 | 70 | /// A type for representing version metadata. 71 | pub struct Version { 72 | /// Major version of the RRG agent (`x` in `x.y.z`). 73 | pub major: u8, 74 | /// Minor version of the RRG agent (`y` in `x.y.z`). 75 | pub minor: u8, 76 | /// Patch version of the RRG agent (`z` in `x.y.z`). 77 | pub patch: u8, 78 | /// Pre-release label of the RRG agent (`foo` in `x.y.z-foo`). 79 | pub pre: &'static str, 80 | } 81 | 82 | impl Version { 83 | 84 | /// Constructs version metadata from Cargo information. 85 | /// 86 | /// This function assumes that are relevant crate information is correctly 87 | /// specified in the `Cargo.toml` file. 88 | pub fn from_cargo() -> Version { 89 | Version { 90 | major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap_or(0), 91 | minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap_or(0), 92 | patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap_or(0), 93 | pre: env!("CARGO_PKG_VERSION_PRE"), 94 | } 95 | } 96 | } 97 | 98 | impl Into for Startup { 99 | 100 | fn into(self) -> rrg_proto::startup::Startup { 101 | use rrg_proto::into_timestamp; 102 | 103 | let mut proto = rrg_proto::startup::Startup::new(); 104 | proto.set_metadata(self.metadata.into()); 105 | if let Some(path) = self.path { 106 | proto.set_path(path.into()); 107 | } 108 | proto.set_args(self.args.into()); 109 | proto.set_agent_startup_time(into_timestamp(self.agent_started)); 110 | proto.set_os_type(self.os_kind.into()); 111 | 112 | proto 113 | } 114 | } 115 | 116 | impl Into for Metadata { 117 | 118 | fn into(self) -> rrg_proto::startup::Metadata { 119 | let mut proto = rrg_proto::startup::Metadata::new(); 120 | proto.set_name(self.name); 121 | // TODO(@panhania): Add support for remaining fields. 122 | proto.set_version(self.version.into()); 123 | 124 | proto 125 | } 126 | } 127 | 128 | impl Into for Version { 129 | 130 | fn into(self) -> rrg_proto::startup::Version { 131 | let mut proto = rrg_proto::startup::Version::new(); 132 | proto.set_major(u32::from(self.major)); 133 | proto.set_minor(u32::from(self.minor)); 134 | proto.set_patch(u32::from(self.patch)); 135 | proto.set_pre(String::from(self.pre)); 136 | 137 | proto 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /crates/ospect/src/os.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | #[cfg(target_os = "linux")] 7 | mod linux; 8 | 9 | #[cfg(target_os = "macos")] 10 | mod macos; 11 | 12 | #[cfg(target_family = "unix")] 13 | mod unix; 14 | 15 | #[cfg(target_os = "windows")] 16 | mod windows; 17 | 18 | mod sys { 19 | #[cfg(target_os = "linux")] 20 | pub use crate::os::linux::*; 21 | 22 | #[cfg(target_os = "macos")] 23 | pub use crate::os::macos::*; 24 | 25 | #[cfg(target_os = "windows")] 26 | pub use crate::os::windows::*; 27 | } 28 | 29 | // TODO(@panhania): Enable the example to run once the method is supported on 30 | // platforms. 31 | /// Returns the time at which the system was installed. 32 | /// 33 | /// Note that this function uses various heuristics to estimate the installation 34 | /// time and they might not really be be accurate. Very often various system 35 | /// updates can "bump" the timestamps that this function considers. 36 | /// 37 | /// # Errors 38 | /// 39 | /// This function will return an error in case there was some error when trying 40 | /// to query data from the system. 41 | /// 42 | /// # Examples 43 | /// 44 | /// ```no_run 45 | /// let time = ospect::os::installed() 46 | /// .unwrap(); 47 | /// 48 | /// assert!(time < std::time::SystemTime::now()); 49 | /// ``` 50 | pub fn installed() -> std::io::Result { 51 | self::sys::installed() 52 | } 53 | 54 | /// A list of operating systems that the library is guaranteed to run on. 55 | pub enum Kind { 56 | Linux, 57 | Macos, 58 | Windows, 59 | } 60 | 61 | /// Returns the [`Kind`] of currently running operating system. 62 | /// 63 | /// [`Kind`]: crate::os::Kind 64 | pub fn kind() -> Kind { 65 | self::sys::kind() 66 | } 67 | 68 | /// Returns the version string of the currently running operating system. 69 | /// 70 | /// No assumptions on the specific format of this string should be made and the 71 | /// output can vary between operating system versions, distributions and even 72 | /// `ospect` releases. 73 | /// 74 | /// # Errors 75 | /// 76 | /// This function will return an error in case there was some issue when trying 77 | /// to query data from the system. 78 | pub fn version() -> std::io::Result { 79 | self::sys::version() 80 | } 81 | 82 | /// Returns the CPU architecture of the currently running operating system. 83 | /// 84 | /// No assumptions on the specific format of this string should be made. Even 85 | /// the same architecture can be reported in different ways depending on the 86 | /// operating system or even its version (e.g. a 64-bit ARM-based architecture 87 | /// can be reported as `arm64` or `aarch64`). 88 | pub fn arch() -> std::io::Result { 89 | self::sys::arch() 90 | } 91 | 92 | /// Returns the hostname of the currently running operating system. 93 | /// 94 | /// # Errors 95 | /// 96 | /// This function will return an error in case there was some issue when trying 97 | /// to query data from the system. 98 | pub fn hostname() -> std::io::Result { 99 | self::sys::hostname() 100 | } 101 | 102 | /// Returns the FQDN of the currently running operating system. 103 | /// 104 | /// Note that FQDN retrival is not reliable on all operating systems and in some 105 | /// cases just a hostname can be returned instead (depending on the configration 106 | /// of the sepcific system). 107 | /// 108 | /// # Errors 109 | /// 110 | /// This function will return an error in case there was some issue when trying 111 | /// to query data from the system. 112 | pub fn fqdn() -> std::io::Result { 113 | self::sys::fqdn() 114 | } 115 | 116 | #[cfg(test)] 117 | mod tests { 118 | 119 | use super::*; 120 | 121 | #[test] 122 | fn version_not_empty() { 123 | assert!(!version().unwrap().is_empty()); 124 | } 125 | 126 | #[test] 127 | fn arch_not_empty() { 128 | assert!(!arch().unwrap().is_empty()); 129 | } 130 | 131 | #[test] 132 | #[cfg_attr(not(target_arch = "x86_64"), ignore)] 133 | fn arch_x86_64() { 134 | assert_eq!(arch().unwrap(), "x86_64"); 135 | } 136 | 137 | #[test] 138 | #[cfg_attr(not(target_arch = "aarch64"), ignore)] 139 | fn arch_aarch64() { 140 | // Some systems report it as `aarch64` (Linux), some as `arm64` (macOS) 141 | // and some give an enum that we "stringify" ourselves so we permit both 142 | // variants. 143 | assert!(matches!(arch().unwrap().as_str(), "aarch64" | "arm64")); 144 | } 145 | 146 | #[test] 147 | fn hostname_not_empty() { 148 | assert!(!hostname().unwrap().is_empty()); 149 | } 150 | 151 | #[test] 152 | fn fqdn_not_empty() { 153 | assert!(!fqdn().unwrap().is_empty()); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /crates/rrg/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rrg" 3 | description = "Rust rewrite of GRR." 4 | version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | 8 | [features] 9 | default = [ 10 | "action-get_system_metadata", 11 | "action-get_file_metadata", 12 | "action-get_file_metadata-md5", 13 | "action-get_file_metadata-sha1", 14 | "action-get_file_metadata-sha256", 15 | "action-get_file_contents", 16 | "action-get_file_sha256", 17 | "action-grep_file_contents", 18 | "action-get_filesystem_timeline", 19 | "action-get_tcp_response", 20 | "action-list_connections", 21 | "action-list_interfaces", 22 | "action-list_mounts", 23 | "action-list_utmp_users", 24 | "action-get_winreg_value", 25 | "action-list_winreg_values", 26 | "action-list_winreg_keys", 27 | "action-query_wmi", 28 | "action-execute_signed_command", 29 | "action-dump_process_memory", 30 | ] 31 | 32 | action-get_system_metadata = [] 33 | action-get_file_metadata = [] 34 | action-get_file_metadata-md5 = ["action-get_file_metadata", "dep:md-5"] 35 | action-get_file_metadata-sha1 = ["action-get_file_metadata", "dep:sha1"] 36 | action-get_file_metadata-sha256 = ["action-get_file_metadata", "dep:sha2"] 37 | action-get_file_contents = ["dep:sha2"] 38 | action-get_file_contents_kmx = ["dep:keramics-core", "dep:keramics-formats", "dep:keramics-types"] 39 | action-get_file_sha256 = ["dep:sha2"] 40 | action-grep_file_contents = [] 41 | action-get_filesystem_timeline = ["dep:flate2", "dep:sha2"] 42 | action-get_filesystem_timeline_tsk = ["action-get_filesystem_timeline", "dep:tsk"] 43 | action-get_tcp_response = [] 44 | action-list_connections = [] 45 | action-list_interfaces = [] 46 | action-list_mounts = [] 47 | action-list_utmp_users = [] 48 | action-get_winreg_value = [] 49 | action-list_winreg_values = [] 50 | action-list_winreg_keys = [] 51 | action-query_wmi = [] 52 | action-execute_signed_command = [] 53 | # TODO: https://github.com/google/rrg/issues/137 54 | # 55 | # This feature exists to prevent preverified commands logic from being available 56 | # in most RRG builds. Once that mechanism is no longer needed, this should be 57 | # deleted. 58 | action-execute_signed_command-preverified = ["action-execute_signed_command"] 59 | action-dump_process_memory = ["dep:windows-sys"] 60 | action-scan_memory_yara = ["action-dump_process_memory", "dep:yara-x"] 61 | 62 | test-setfattr = [] 63 | test-chattr = [] 64 | test-fuse = ["dep:fuse"] 65 | test-libguestfs = [] 66 | test-wtmp = [] 67 | 68 | [dependencies.ospect] 69 | path = "../ospect" 70 | 71 | [dependencies.rrg-proto] 72 | path = "../rrg-proto" 73 | 74 | [dependencies.tsk] 75 | path = "../tsk" 76 | optional = true 77 | 78 | [dependencies.winreg] 79 | path = "../winreg" 80 | 81 | [dependencies.wmi] 82 | path = "../wmi" 83 | 84 | [dependencies.argh] 85 | version = "0.1.12" 86 | 87 | [dependencies.fleetspeak] 88 | version = "0.4.2" 89 | 90 | [dependencies.humantime] 91 | version = "2.1.0" 92 | 93 | [dependencies.libc] 94 | version = "0.2.161" 95 | 96 | [dependencies.log] 97 | version = "0.4.22" 98 | features = [ 99 | "std", 100 | ] 101 | 102 | [dependencies.protobuf] 103 | version = "3.7.2" 104 | 105 | [dependencies.regex] 106 | version = "1.11.3" 107 | 108 | [dependencies.lazy_static] 109 | version = "1.5.0" 110 | 111 | [dependencies.digest] 112 | version = "0.10.7" 113 | optional = true 114 | 115 | [dependencies.flate2] 116 | version = "1.0.34" 117 | optional = true 118 | 119 | [dependencies.keramics-core] 120 | version = "0.0.0" 121 | optional = true 122 | 123 | [dependencies.keramics-formats] 124 | version = "0.0.0" 125 | optional = true 126 | 127 | [dependencies.keramics-types] 128 | version = "0.0.0" 129 | optional = true 130 | 131 | [dependencies.md-5] 132 | version = "0.10.6" 133 | optional = true 134 | 135 | [dependencies.sha1] 136 | version = "0.10.6" 137 | optional = true 138 | 139 | [dependencies.sha2] 140 | version = "0.10.8" 141 | optional = true 142 | 143 | # TODO(https://github.com/google/rrg/issues/47): This should be a dev dependency 144 | # but because of Cargo limitations [1] it has to be marked not as such. However, 145 | # because it is hidden behind a feature flag, it should not be a big problem. 146 | # 147 | # [1]: https://github.com/rust-lang/cargo/issues/1596 148 | [target.'cfg(target_os = "linux")'.dependencies.fuse] 149 | version = "0.3.1" 150 | optional = true 151 | 152 | [dev-dependencies.rand] 153 | version = "0.8.5" 154 | 155 | [dev-dependencies.tempfile] 156 | version = "3.13.0" 157 | 158 | [dev-dependencies.quickcheck] 159 | version = "1.0.3" 160 | 161 | [target.'cfg(target_family = "windows")'.dependencies.windows-sys] 162 | version = "0.59.0" 163 | optional = true 164 | features = [ 165 | "Win32_Foundation", 166 | "Win32_Storage_FileSystem", 167 | "Win32_System_Diagnostics_Debug", 168 | "Win32_System_Memory", 169 | "Win32_System_ProcessStatus", 170 | "Win32_System_Threading", 171 | ] 172 | 173 | [target.'cfg(target_family = "windows")'.dev-dependencies.windows-sys] 174 | version = "0.59.0" 175 | features = [ 176 | "Win32_Foundation", 177 | "Win32_Storage_FileSystem", 178 | ] 179 | 180 | [dependencies.ed25519-dalek] 181 | version = "2.1.1" 182 | 183 | [dev-dependencies.ed25519-dalek] 184 | version = "2.1.1" 185 | features = [ 186 | "rand_core", 187 | ] 188 | 189 | [dependencies.yara-x] 190 | version = "1.8.1" 191 | optional = true 192 | default-features = false 193 | -------------------------------------------------------------------------------- /crates/ospect/src/os/unix.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | /// Returns the time at which the system was installed. 7 | pub fn installed() -> std::io::Result { 8 | let root_metadata = std::fs::metadata("/")?; 9 | root_metadata.created() 10 | } 11 | 12 | /// Returns the version string of the currently running operating system. 13 | pub fn version() -> std::io::Result { 14 | let uname = uname()?; 15 | 16 | // SAFETY: All strings in `utsname` are guaranteed to be null-terminated. As 17 | // mentioned, the buffer is valid for the entire scope of the function and 18 | // we create an owned copy before we return, so the call is safe. 19 | Ok(unsafe { 20 | std::ffi::CStr::from_ptr(uname.version.as_ptr()) 21 | }.to_string_lossy().into_owned()) 22 | } 23 | 24 | /// Returns the CPU architecture of the currently running operating system. 25 | pub fn arch() -> std::io::Result { 26 | let uname = uname()?; 27 | 28 | // SAFETY: All strings in `utsname` are guaranteed to be null-terminated. 29 | // The buffer is valid for the entire scope of the function and we create 30 | // create an owned copy before we return, so the call is safe. 31 | Ok(unsafe { 32 | std::ffi::CStr::from_ptr(uname.machine.as_ptr()) 33 | }.to_string_lossy().into_owned()) 34 | } 35 | 36 | /// Returns the hostname of the currently running operating system. 37 | pub fn hostname() -> std::io::Result { 38 | let uname = uname()?; 39 | 40 | // SAFETY: All strings in `utsname` are guaranteed to be null-terminated. As 41 | // mentioned, the buffer is valid for the entire scope of the function and 42 | // we create an owned copy before we return, so the call is safe. 43 | let hostname = unsafe { 44 | std::ffi::CStr::from_ptr(uname.nodename.as_ptr()) 45 | }; 46 | 47 | use std::os::unix::ffi::OsStrExt as _; 48 | let hostname = std::ffi::OsStr::from_bytes(hostname.to_bytes()); 49 | 50 | Ok(hostname.to_os_string()) 51 | } 52 | 53 | /// Returns the FQDN of the currently running operating system. 54 | pub fn fqdn() -> std::io::Result { 55 | let uname = uname()?; 56 | 57 | let hints = libc::addrinfo { 58 | ai_family: libc::AF_UNSPEC, // `AF_UNSPEC` means "any family". 59 | ai_socktype: 0, // 0 means "any type". 60 | ai_protocol: 0, // 0 means "any protocol". 61 | ai_flags: libc::AI_CANONNAME, 62 | // The following fields are irrelevant for `getaddrinfo` call. 63 | ai_addrlen: 0, 64 | ai_addr: std::ptr::null_mut(), 65 | ai_canonname: std::ptr::null_mut(), 66 | ai_next: std::ptr::null_mut(), 67 | }; 68 | 69 | let mut info = std::mem::MaybeUninit::uninit(); 70 | 71 | // SAFETY: We call the function as described in the documentation [1] and 72 | // verify the return code below. In case of success, we free the memory at 73 | // the end of the function. 74 | // 75 | // [1]: https://man7.org/linux/man-pages/man3/getaddrinfo.3.html 76 | let code = unsafe { 77 | libc::getaddrinfo( 78 | uname.nodename.as_ptr(), 79 | std::ptr::null(), 80 | &hints, 81 | info.as_mut_ptr(), 82 | ) 83 | }; 84 | if code != 0 { 85 | // Ideally, we should use `gai_strerror` to get a human-friendly message 86 | // of the error. Unfortunately, it is not clear whether this function is 87 | // or is not thread-safe so we just return a generic error. 88 | use std::io::{Error, ErrorKind::Other}; 89 | return Err(Error::new(Other, "`getaddrinfo` failure")) 90 | } 91 | 92 | // SAFETY: We verified that the call succeeded. It means that the call has 93 | // initialized the pointer and we can read from it. 94 | let info = unsafe { 95 | info.assume_init() 96 | }; 97 | 98 | let fqdn = { 99 | // SAFETY: We have verified that the call for which we specified the 100 | // `AI_CANONNAME` flag succeeded, to the `ai_canonname` is pointing to 101 | // the name of the host. We create a scoped reference that is used then 102 | // copied to an owned value and free the memory afterwards. 103 | let fqdn = unsafe { 104 | std::ffi::CStr::from_ptr((*info).ai_canonname) 105 | }; 106 | 107 | use std::os::unix::ffi::OsStrExt as _; 108 | std::ffi::OsStr::from_bytes(fqdn.to_bytes()).to_os_string() 109 | }; 110 | 111 | // SAFETY: `fqdn` has been copied and no references are kept around, so we 112 | // can release the memory now. 113 | unsafe { 114 | libc::freeaddrinfo(info); 115 | } 116 | 117 | Ok(fqdn) 118 | } 119 | 120 | /// Returns `uname` information of the currently running operating system. 121 | fn uname() -> std::io::Result { 122 | let mut uname = std::mem::MaybeUninit::uninit(); 123 | 124 | // SAFETY: We just pass the buffer we allocated. The buffer is valid for the 125 | // entire scope of this function. 126 | let code = unsafe { 127 | libc::uname(uname.as_mut_ptr()) 128 | }; 129 | if code < 0 { 130 | return Err(std::io::Error::last_os_error()); 131 | } 132 | 133 | // SAFETY: We verified that the call succeeded. It means that the call has 134 | // initialized the buffer and we can read from it. 135 | let uname = unsafe { 136 | uname.assume_init() 137 | }; 138 | 139 | Ok(uname) 140 | } 141 | -------------------------------------------------------------------------------- /crates/rrg/src/session/fake.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | use crate::Sink; 4 | 5 | /// A session implementation intended to be used in tests. 6 | /// 7 | /// Testing actions with normal session objects can be quite hard, since 8 | /// they communicate with the outside world (through Fleetspeak). Since we 9 | /// want to keep the tests minimal and not waste resources on unneeded I/O, 10 | /// using real sessions is not an option. 11 | /// 12 | /// Instead, one can use a `Fake` session. It simply accumulates responses 13 | /// that the action sends and lets the creator inspect them later. 14 | pub struct FakeSession { 15 | args: crate::args::Args, 16 | replies: Vec>, 17 | parcels: std::collections::HashMap>>, 18 | } 19 | 20 | impl FakeSession { 21 | 22 | /// Constructs a new fake session with test default agent arguments. 23 | pub fn new() -> FakeSession { 24 | FakeSession::with_args(crate::args::Args { 25 | heartbeat_rate: std::time::Duration::from_secs(0), 26 | ping_rate: std::time::Duration::from_secs(0), 27 | command_verification_key: Some(ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng).verifying_key()), 28 | verbosity: log::LevelFilter::Debug, 29 | log_to_stdout: false, 30 | log_to_file: None, 31 | }) 32 | } 33 | 34 | /// Constructs a new fake session with the given agent arguments. 35 | pub fn with_args(args: crate::args::Args) -> FakeSession { 36 | FakeSession { 37 | args, 38 | replies: Vec::new(), 39 | parcels: std::collections::HashMap::new(), 40 | } 41 | } 42 | 43 | /// Yields the number of replies that this session sent so far. 44 | pub fn reply_count(&self) -> usize { 45 | self.replies.len() 46 | } 47 | 48 | /// Retrieves a reply corresponding to the given id. 49 | /// 50 | /// The identifier corresponding to the first response is 0, the second one 51 | /// is 1 and so on. 52 | /// 53 | /// This method will panic if a reply with the specified `id` does not exist 54 | /// or if it exists but has a wrong type. 55 | pub fn reply(&self, id: usize) -> &R 56 | where 57 | R: crate::response::Item + 'static, 58 | { 59 | match self.replies().nth(id) { 60 | Some(reply) => reply, 61 | None => panic!("no reply #{}", id), 62 | } 63 | } 64 | 65 | /// Constructs an iterator over session replies. 66 | /// 67 | /// The iterator will panic (but not immediately) if some reply has an 68 | /// incorrect type. 69 | pub fn replies(&self) -> impl Iterator 70 | where 71 | R: crate::response::Item + 'static 72 | { 73 | self.replies.iter().map(|reply| { 74 | reply.downcast_ref().expect("unexpected reply type") 75 | }) 76 | } 77 | 78 | /// Yields the number of parcels sent so far to the specified sink. 79 | pub fn parcel_count(&self, sink: Sink) -> usize { 80 | match self.parcels.get(&sink) { 81 | Some(parcels) => parcels.len(), 82 | None => 0, 83 | } 84 | } 85 | 86 | /// Retrieves a parcel with the given id sent to a particular sink. 87 | /// 88 | /// The identifier corresponding to the first parcel to the particular sink 89 | /// is 0, to the second one (to the same sink) is 1 and so on. 90 | /// 91 | /// This method will panic if a reply with the specified `id` to the given 92 | /// `sink` does not exist or if it exists but has wrong type. 93 | pub fn parcel(&self, sink: Sink, id: usize) -> &I 94 | where 95 | I: crate::response::Item + 'static, 96 | { 97 | match self.parcels(sink).nth(id) { 98 | Some(parcel) => parcel, 99 | None => panic!("no parcel #{} for sink '{:?}'", id, sink), 100 | } 101 | } 102 | 103 | /// Constructs an iterator over session parcels for the given sink. 104 | /// 105 | /// The iterator will panic (but not immediately) if some parcels have an 106 | /// incorrect type. 107 | pub fn parcels(&self, sink: Sink) -> impl Iterator 108 | where 109 | I: crate::response::Item + 'static, 110 | { 111 | // Since the empty iterator (as defined in the standard library) is a 112 | // specific type, it cannot be returned in one branch but not in another 113 | // branch. 114 | // 115 | // Instead, we use the fact that `Option` is an iterator and then we 116 | // squash it with `Iterator::flatten`. 117 | let parcels = self.parcels.get(&sink).into_iter().flatten(); 118 | 119 | parcels.map(move |parcel| match parcel.downcast_ref() { 120 | Some(parcel) => parcel, 121 | None => panic!("unexpected parcel type in sink '{:?}'", sink), 122 | }) 123 | } 124 | } 125 | 126 | impl crate::session::Session for FakeSession { 127 | 128 | fn args(&self) -> &crate::args::Args { 129 | &self.args 130 | } 131 | 132 | fn reply(&mut self, item: I) -> crate::session::Result<()> 133 | where 134 | I: crate::response::Item + 'static, 135 | { 136 | self.replies.push(Box::new(item)); 137 | 138 | Ok(()) 139 | } 140 | 141 | fn send(&mut self, sink: Sink, item: I) -> crate::session::Result<()> 142 | where 143 | I: crate::response::Item + 'static, 144 | { 145 | let parcels = self.parcels.entry(sink).or_insert_with(Vec::new); 146 | parcels.push(Box::new(item)); 147 | 148 | Ok(()) 149 | } 150 | 151 | fn heartbeat(&mut self) { 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /crates/rrg/src/action/get_winreg_value.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | #[cfg(target_family = "windows")] 7 | /// Arguments of the `get_winreg_value` action. 8 | pub struct Args { 9 | /// Root predefined key of the value to get. 10 | root: winreg::PredefinedKey, 11 | /// Key relative to `root` of the value to get (e.g. `SOFTWARE\Microsoft`). 12 | key: std::ffi::OsString, 13 | /// Name of the value to get. 14 | value_name: std::ffi::OsString, 15 | } 16 | 17 | /// A result of the `get_winreg_value` action. 18 | #[cfg(target_family = "windows")] 19 | struct Item { 20 | /// Root predefined key of the retrieved value. 21 | root: winreg::PredefinedKey, 22 | /// Key relative to `root` of the retrieved value. 23 | key: std::ffi::OsString, 24 | /// Retrieved value. 25 | value: winreg::Value, 26 | } 27 | 28 | /// Handles invocations of the `get_winreg_value` action. 29 | #[cfg(target_family = "unix")] 30 | pub fn handle(_: &mut S, _: ()) -> crate::session::Result<()> 31 | where 32 | S: crate::session::Session, 33 | { 34 | use std::io::{Error, ErrorKind}; 35 | Err(crate::session::Error::action(Error::from(ErrorKind::Unsupported))) 36 | } 37 | 38 | /// Handles invocations of the `get_winreg_value` action. 39 | #[cfg(target_family = "windows")] 40 | pub fn handle(session: &mut S, args: Args) -> crate::session::Result<()> 41 | where 42 | S: crate::session::Session, 43 | { 44 | let key = args.root.open(&args.key) 45 | .map_err(crate::session::Error::action)?; 46 | 47 | let value_data = key.value_data(&args.value_name) 48 | .map_err(crate::session::Error::action)?; 49 | 50 | session.reply(Item { 51 | root: args.root, 52 | // TODO(@panhania): Add support for case-correcting the key. 53 | key: args.key, 54 | value: winreg::Value { 55 | // TODO(@panhania): Add support for case-correcting the value. 56 | name: args.value_name, 57 | data: value_data, 58 | } 59 | })?; 60 | 61 | Ok(()) 62 | } 63 | 64 | #[cfg(target_family = "windows")] 65 | impl crate::request::Args for Args { 66 | 67 | type Proto = rrg_proto::get_winreg_value::Args; 68 | 69 | fn from_proto(mut proto: Self::Proto) -> Result { 70 | let root = match proto.root.enum_value() { 71 | Ok(root) => winreg::PredefinedKey::try_from(root), 72 | Err(value) => Err(rrg_proto::ParseWinregPredefinedKeyError { value }), 73 | }.map_err(|error| { 74 | crate::request::ParseArgsError::invalid_field("root", error) 75 | })?; 76 | 77 | Ok(Args { 78 | root, 79 | key: std::ffi::OsString::from(proto.take_key()), 80 | value_name: std::ffi::OsString::from(proto.take_name()), 81 | }) 82 | } 83 | } 84 | 85 | #[cfg(target_family = "windows")] 86 | impl crate::response::Item for Item { 87 | 88 | type Proto = rrg_proto::get_winreg_value::Result; 89 | 90 | fn into_proto(self) -> Self::Proto { 91 | let mut proto = rrg_proto::get_winreg_value::Result::new(); 92 | proto.set_root(self.root.into()); 93 | proto.set_key(self.key.to_string_lossy().into_owned()); 94 | proto.set_value(self.value.into()); 95 | 96 | proto 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | #[cfg(target_family = "windows")] 102 | mod tests { 103 | 104 | use super::*; 105 | 106 | #[test] 107 | fn handle_non_existent() { 108 | let args = Args { 109 | root: winreg::PredefinedKey::LocalMachine, 110 | key: std::ffi::OsString::from("FOOWARE\\Linux\\GNU"), 111 | value_name: std::ffi::OsString::from("Version"), 112 | }; 113 | 114 | let mut session = crate::session::FakeSession::new(); 115 | assert!(handle(&mut session, args).is_err()); 116 | } 117 | 118 | #[test] 119 | fn handle_string() { 120 | let args = Args { 121 | root: winreg::PredefinedKey::LocalMachine, 122 | key: std::ffi::OsString::from("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"), 123 | value_name: std::ffi::OsString::from("CurrentType"), 124 | }; 125 | 126 | let mut session = crate::session::FakeSession::new(); 127 | assert!(handle(&mut session, args).is_ok()); 128 | assert_eq!(session.reply_count(), 1); 129 | 130 | let item = session.reply::(0); 131 | assert_eq!(item.root, winreg::PredefinedKey::LocalMachine); 132 | assert_eq!(item.key, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"); 133 | assert_eq!(item.value.name, "CurrentType"); 134 | assert!(matches!(item.value.data, winreg::ValueData::String(_))); 135 | } 136 | 137 | #[test] 138 | fn handle_bytes() { 139 | let args = Args { 140 | root: winreg::PredefinedKey::LocalMachine, 141 | key: std::ffi::OsString::from("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"), 142 | value_name: std::ffi::OsString::from("DigitalProductId"), 143 | }; 144 | 145 | let mut session = crate::session::FakeSession::new(); 146 | assert!(handle(&mut session, args).is_ok()); 147 | assert_eq!(session.reply_count(), 1); 148 | 149 | let item = session.reply::(0); 150 | assert_eq!(item.root, winreg::PredefinedKey::LocalMachine); 151 | assert_eq!(item.key, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"); 152 | assert_eq!(item.value.name, "DigitalProductId"); 153 | assert!(matches!(item.value.data, winreg::ValueData::Bytes(_))); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /docs/creating-actions.md: -------------------------------------------------------------------------------- 1 | Creating actions 2 | ================ 3 | 4 | Creating new actions is easy but requires some boilerplate to be written first. 5 | You can take a look at commit [`845c87b`] to see an example of that. Follow the 6 | steps outlined in this guide for more details. 7 | 8 | ### Define Protocol Buffers messages 9 | 10 | Each action should define its arguments and result as Protocol Buffer messages 11 | in its own package using the `proto3` syntax. All `.proto` definitions live in 12 | the [`proto/rrg/action`][1] subfolder. The file (and the package) should have 13 | the same name as the action it corresponds to and should contain at least two 14 | messages named `Args` and `Result` and have an appropriate license header. 15 | 16 | For example, when implementing an action called `list_foo`, you should create 17 | a `proto/rrg/action/list_foo.proto` file that looks like this: 18 | 19 | ~~~protobuf 20 | // Copyright 2023 Google LLC 21 | // 22 | // Use of this source code is governed by an MIT-style license that can be found 23 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 24 | syntax = "proto3"; 25 | 26 | package rrg.action.list_foo; 27 | 28 | message Args { 29 | // TODO. 30 | } 31 | 32 | message Result { 33 | // TODO. 34 | } 35 | ~~~ 36 | 37 | The path to the created file needs to be added to the [build script][2] of the 38 | `rrg-proto` crate. 39 | 40 | ### Define a Cargo feature 41 | 42 | Every action needs to be hidden behind a feature. This way actions that are 43 | irrelevant for specific deployments can be completely compiled-out from the 44 | agent executable making it smaller and potentially more secure. 45 | 46 | Action features are defined in the [Cargo manifest][3] of the main `rrg` crate 47 | and should use `action-` prefix. For example, a feature flag for the `list_foo` 48 | action would be named `action-list_foo`. You can also add the feature to the 49 | list of default features, if it makes sense for your action. 50 | 51 | If your action needs some third-party dependencies that are specific to it, make 52 | them optional and use feature dependencies to specify them, e.g.: 53 | 54 | ~~~toml 55 | action-list_foo = ["dep:bar", "dep:baz"] 56 | 57 | (...) 58 | 59 | [dependencies.bar] 60 | version = "1.33.7" 61 | optional = true 62 | 63 | [dependencies.baz] 64 | version = "0.42.0" 65 | optional = true 66 | ~~~ 67 | 68 | ### Create a Rust module 69 | 70 | All actions should be defined in their own modules as children of the parent 71 | [`rrg::action`] module and be named the same as the action itself. This module 72 | should define idiomatic Rust types corresponding to the argument and result 73 | messages defined in the `.proto` file. Unfortunately, because `Result` is an 74 | already established concept in Rust, the type corresponding to the `Result` 75 | message has to be named differently—in Rust code we use `Item` instead. These 76 | types should implement `rrg::request::Args` and `rrg::request::Item` traits to 77 | convert between idiomatic Rust and Protocol Buffers types. Finally, the module 78 | should implement a `handle` method that executes the action. 79 | 80 | For example, when implementing an action called `list_foo`, you should create 81 | a `crates/rrg/src/action/list_foo.rs` file that looks like this: 82 | 83 | ~~~rust 84 | // Copyright 2023 Google LLC 85 | // 86 | // Use of this source code is governed by an MIT-style license that can be found 87 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 88 | 89 | /// Arguments of the `list_foo` action. 90 | pub struct Args { 91 | // TODO. 92 | } 93 | 94 | /// Result of the `list_foo` action. 95 | pub struct Item { 96 | // TODO. 97 | } 98 | 99 | /// Handles invocations of the `list_foo` action. 100 | pub fn handle(session: &mut S, args: Args) -> crate::session::Result<()> 101 | where 102 | S: crate::session::Session, 103 | { 104 | todo!() 105 | } 106 | 107 | impl crate::request::Args for Args { 108 | 109 | type Proto = rrg_proto::list_foo::Args; 110 | 111 | fn from_proto(mut proto: Self::Proto) -> Result { 112 | todo!() 113 | } 114 | } 115 | 116 | impl crate::response::Item for Item { 117 | 118 | type Proto = rrg_proto::list_foo::Result; 119 | 120 | fn into_proto(self) -> Self::Proto { 121 | todo!() 122 | } 123 | } 124 | ~~~ 125 | 126 | This file has to be declared as a child of the [`rrg::action`] module and should 127 | be hidden behind the feature declared earlier: 128 | 129 | ~~~rust 130 | #[cfg(feature = "action-list_foo")] 131 | pub mod list_foo; 132 | ~~~ 133 | 134 | ### Register the action 135 | 136 | Finally, the action has to be registered so that requests can actually be routed 137 | to invoke it. 138 | 139 | First, you need to extend the [RRG protocol][4]. Add the new variant to the 140 | `Action` enum that has the same name as the action. If you are contributing 141 | upstream, pick and use the first available field number. If you are developing 142 | an action that is internal to your deployment, use one of the field numbers from 143 | the reserved range between 1024 and 2048. 144 | 145 | Once the Protocol Buffers enum has the new field, add a new variant to the Rust 146 | `Action` enum defined in the [`rrg::request`] module. The compiler and the tests 147 | will guide you to towards updating the existing code to cover the new variant in 148 | all the required branches. 149 | 150 | As the last step, update the `dispatch` function in the [`rrg::action`] module 151 | and route the call to the `handle` function you defined. Remember to guard the 152 | branch with the corresponding feature to avoid issues when compiling with the 153 | feature disabled. 154 | 155 | 156 | [1]: https://github.com/google/rrg/blob/master/proto/rrg/action 157 | [2]: https://github.com/google/rrg/blob/master/crates/rrg-proto/build.rs 158 | [3]: https://github.com/google/rrg/blob/master/crates/rrg/Cargo.toml 159 | [4]: https://github.com/google/rrg/blob/master/proto/rrg.proto 160 | 161 | [`rrg::action`]: https://github.com/google/rrg/blob/master/crates/rrg/src/action.rs 162 | [`rrg::request`]: https://github.com/google/rrg/blob/master/crates/rrg/src/request.rs 163 | 164 | [`845c87b`]: https://github.com/google/rrg/commit/845c87b7c3373abacf41a17729ad95e1d6ab046a 165 | -------------------------------------------------------------------------------- /crates/wmi/src/windows/bstr.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | /// Owned wrapper around [`windows_sys::core::BSTR`]. 7 | pub struct BString(windows_sys::core::BSTR); 8 | 9 | impl BString { 10 | 11 | /// Creates a new `BSTR` wrapper for the given string. 12 | /// 13 | /// # Panics 14 | /// 15 | /// If the given string length exceeds [`u32::MAX`] characters. 16 | pub fn new>(string: S) -> BString { 17 | use std::os::windows::ffi::OsStrExt as _; 18 | 19 | let string_wide = string.as_ref().encode_wide().collect::>(); 20 | let string_wide_len = match u32::try_from(string_wide.len()) { 21 | Ok(string_wide_len) => string_wide_len, 22 | Err(_) => panic!("string too long"), 23 | }; 24 | 25 | // SAFETY: Simple FFI call as described in the documentation [1]. 26 | // 27 | // [1]: https://learn.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-sysallocstringlen 28 | let ptr = unsafe { 29 | windows_sys::Win32::Foundation::SysAllocStringLen( 30 | string_wide.as_ptr(), 31 | string_wide_len, 32 | ) 33 | }; 34 | 35 | // The call can return null only in case of insufficient memory [1]. 36 | // 37 | // [1]: https://learn.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-sysallocstring#return-value 38 | if ptr == std::ptr::null() { 39 | panic!("out of memory") 40 | } 41 | 42 | BString(ptr) 43 | } 44 | 45 | /// Creates a new `BSTR` wrapper from raw pointer and takes ownership. 46 | /// 47 | /// # Safety 48 | /// 49 | /// The pointer must be valid instance of `BSTR`. It has similar semantics 50 | /// and requirements as [`Box::from_raw`]. 51 | pub unsafe fn from_raw_bstr(raw: windows_sys::core::BSTR) -> BString { 52 | BString(raw) 53 | } 54 | 55 | /// Copies the string into an owned [`std::ffi::OsString`]. 56 | pub fn to_os_string(&self) -> std::ffi::OsString { 57 | self.as_bstr().to_os_string() 58 | } 59 | 60 | /// Converts the string to its borrowed variant. 61 | pub fn as_bstr<'a>(&'a self) -> BStr<'a> { 62 | // SAFETY: We hold a valid `BSTR` instance with guaranteed lifetime. 63 | unsafe { 64 | BStr::from_raw_bstr(self.0) 65 | } 66 | } 67 | 68 | /// Returns the raw `BSTR` backing the string. 69 | pub fn as_raw_bstr(&self) -> windows_sys::core::BSTR { 70 | self.0 71 | } 72 | } 73 | 74 | impl Drop for BString { 75 | 76 | fn drop(&mut self) { 77 | // SAFETY: Simple FFI call as described in the documentation [1]. Type 78 | // system guarantees that the pointer has not been freed yet. 79 | // 80 | // [1]: https://learn.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-sysfreestring 81 | unsafe { 82 | windows_sys::Win32::Foundation::SysFreeString(self.0) 83 | } 84 | } 85 | } 86 | 87 | /// Borrowed wrapper around [`windows_sys::core::BSTR`]. 88 | #[derive(Copy, Clone)] 89 | pub struct BStr<'a> { 90 | raw: windows_sys::core::BSTR, 91 | phantom: std::marker::PhantomData<&'a ()>, 92 | } 93 | 94 | impl<'a> BStr<'a> { 95 | 96 | /// Creates a new `BSTR` wrapper from raw pointer without taking ownership. 97 | /// 98 | /// # Safety 99 | /// 100 | /// The pointer must be valid instance of `BSTR`. It has similar semantics 101 | /// and requirements as [`std::slice::from_raw_parts`]. 102 | pub unsafe fn from_raw_bstr(raw: windows_sys::core::BSTR) -> BStr<'a> { 103 | BStr { 104 | raw: raw, 105 | phantom: std::marker::PhantomData, 106 | } 107 | } 108 | 109 | /// Returns the length of the string in bytes. 110 | pub fn count_bytes(self) -> usize { 111 | // SAFETY: Every `BSTR` instance is prefixed with 4-byte length of the 112 | // string (excluding the null terminator) [1]. This value is placed 113 | // directly *before* the pointer that we have, so we offset it and read 114 | // from there. 115 | // 116 | // [1] https://learn.microsoft.com/en-us/previous-versions/windows/desktop/automat/bstr#remarks 117 | unsafe { 118 | *self.as_raw_bstr().cast::().offset(-4).cast::() as usize 119 | } 120 | } 121 | 122 | /// Copies the string into an owned [`std::ffi::OsString`]. 123 | pub fn to_os_string(self) -> std::ffi::OsString { 124 | let len = self.count_bytes() / std::mem::size_of::(); 125 | 126 | // SAFETY: We know that the pointer is valid and calculate its length by 127 | // taking it byte length and dividing by the size of each character (so, 128 | // 2 bytes). 129 | std::os::windows::ffi::OsStringExt::from_wide(unsafe { 130 | std::slice::from_raw_parts(self.as_raw_bstr(), len) 131 | }) 132 | } 133 | 134 | /// Returns the raw `BSTR` backing the string. 135 | pub fn as_raw_bstr(self) -> windows_sys::core::BSTR { 136 | self.raw 137 | } 138 | } 139 | 140 | #[cfg(test)] 141 | mod tests { 142 | 143 | use super::*; 144 | 145 | #[test] 146 | fn bstring_from_str_empty() { 147 | let _ = BString::new(""); 148 | } 149 | 150 | #[test] 151 | fn bstring_from_str_ascii() { 152 | let _ = BString::new("foobar"); 153 | } 154 | 155 | #[test] 156 | fn bstring_from_str_unicode() { 157 | let _ = BString::new("załóć gęślą jaźń"); 158 | } 159 | 160 | #[test] 161 | fn bstring_to_os_string_empty() { 162 | assert_eq!(BString::new("").to_os_string(), ""); 163 | } 164 | 165 | #[test] 166 | fn bstring_to_os_string_ascii() { 167 | assert_eq!(BString::new("foobar").to_os_string(), "foobar"); 168 | } 169 | 170 | #[test] 171 | fn bstring_to_os_string_unicode() { 172 | assert_eq!(BString::new("żółć").to_os_string(), "żółć"); 173 | } 174 | 175 | #[test] 176 | fn bstr_count_bytes_empty() { 177 | assert_eq!(BString::new("").as_bstr().count_bytes(), 0); 178 | } 179 | 180 | #[test] 181 | fn bstr_count_bytes_ascii() { 182 | assert_eq!(BString::new("foobar").as_bstr().count_bytes(), 12); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /crates/rrg/src/args.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | //! Structured specification of command-line arguments. 7 | //! 8 | //! This module specifies all command-line arguments that RRG offers and exposes 9 | //! functions for parsing them into a high-level structure. 10 | //! 11 | //! Ideally, only one instance of this high-level structure should ever be 12 | //! created (using the [`from_env_args`] function). Then this instance should be 13 | //! shared through the entire lifetime of a program and explicitly passed to 14 | //! functions that care about it. 15 | //! 16 | //! [`from_env_args`]: fn.from_env_args.html 17 | 18 | use std::time::Duration; 19 | 20 | #[derive(argh::FromArgs)] 21 | /// A GRR agent written in Rust. 22 | pub struct Args { 23 | /// A frequency of heartbeat messages to send to the Fleetspeak client. 24 | #[argh(option, 25 | long="heartbeat-rate", 26 | arg_name="DURATION", 27 | default="::std::time::Duration::from_secs(5)", 28 | description="frequency of heartbeat messages sent to Fleetspeak", 29 | from_str_fn(parse_duration))] 30 | pub heartbeat_rate: Duration, 31 | 32 | // TODO(@panhania): Remove once no longer needed. 33 | /// A frequency of ping messages to send to the GRR server. 34 | #[argh(option, 35 | long="ping-rate", 36 | arg_name="DURATION", 37 | // TODO(@panhania): Set the default to 30 minutes once the ping sink is 38 | // supported by the GRR server. 39 | default="::std::time::Duration::ZERO", 40 | description="frequency of ping messages sent to GRR (0 means never)", 41 | from_str_fn(parse_duration))] 42 | pub ping_rate: Duration, 43 | 44 | /// A verbosity of logging. 45 | #[argh(option, 46 | long="verbosity", 47 | arg_name="LEVEL", 48 | description="level of logging verbosity", 49 | default="::log::LevelFilter::Info")] 50 | pub verbosity: log::LevelFilter, 51 | 52 | /// Determines whether to log to the standard output. 53 | #[argh(switch, 54 | long="log-to-stdout", 55 | description="whether to log to standard output")] 56 | pub log_to_stdout: bool, 57 | 58 | /// Determines whether to log to a file (and where). 59 | #[argh(option, 60 | long="log-to-file", 61 | arg_name="PATH", 62 | description="whether to log to a file")] 63 | pub log_to_file: Option, 64 | 65 | /// The public key for verfying signed commands. 66 | #[argh(option, 67 | long="command-verification-key", 68 | arg_name="KEY", 69 | description="verification key for signed commands", 70 | from_str_fn(parse_verfication_key))] 71 | pub command_verification_key: Option, 72 | } 73 | 74 | /// Parses command-line arguments. 75 | /// 76 | /// This is a just a convenience function intended to be used as a shortcut for 77 | /// creating instances of [`Args`]. Ideally, it should be called only once in 78 | /// the entire lifetime of the agent. 79 | /// 80 | /// [`Args`]: struct.Args.html 81 | pub fn from_env_args() -> Args { 82 | argh::from_env() 83 | } 84 | 85 | /// Parses a human-friendly duration description to a `Duration` object. 86 | fn parse_duration(value: &str) -> Result { 87 | humantime::parse_duration(value).map_err(|error| error.to_string()) 88 | } 89 | 90 | /// Decodes a slice of hex digits to a Vector of byte values. 91 | fn decode_hex(hex: &str) -> Result, DecodeHexError> { 92 | use DecodeHexError::*; 93 | 94 | // TODO(rust-lang/rust#74985): Use `array_chunks` once stabilized. 95 | let chars = hex.chars().collect::>(); 96 | let pairs = chars.chunks_exact(2); 97 | if !pairs.remainder().is_empty() { 98 | return Err(InvalidLen(chars.len())); 99 | } 100 | 101 | pairs.map(|pair| { 102 | let hi = pair[0].to_digit(16).ok_or(InvalidChar(pair[0]))? as u8; 103 | let lo = pair[1].to_digit(16).ok_or(InvalidChar(pair[1]))? as u8; 104 | Ok(hi << 4 | lo) 105 | }).collect() 106 | } 107 | 108 | #[derive(Debug)] 109 | enum DecodeHexError { 110 | InvalidLen(usize), 111 | InvalidChar(char), 112 | } 113 | 114 | impl std::fmt::Display for DecodeHexError { 115 | 116 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 117 | match *self { 118 | DecodeHexError::InvalidLen(len) => { 119 | write!(f, "invalid hex string length: {len}") 120 | } 121 | DecodeHexError::InvalidChar(char) => { 122 | write!(f, "invalid hex character: {char}") 123 | } 124 | } 125 | } 126 | } 127 | 128 | /// Parses a ed25519 verification key from hex data given as string to a `VerifyingKey` object. 129 | fn parse_verfication_key(key: &str) -> Result { 130 | let bytes = decode_hex(key).map_err(|error| error.to_string())?; 131 | ed25519_dalek::VerifyingKey::try_from(&bytes[..]).map_err(|error| error.to_string()) 132 | } 133 | 134 | #[cfg(test)] 135 | mod test { 136 | 137 | use super::*; 138 | 139 | use quickcheck::quickcheck; 140 | 141 | #[test] 142 | fn decode_hex_capital_letters() { 143 | assert_eq!(decode_hex("A28F").unwrap(), vec![0xA2, 0x8F]) 144 | } 145 | 146 | #[test] 147 | fn decode_hex_lower_case_letters() { 148 | assert_eq!(decode_hex("a28f").unwrap(), vec![0xa2, 0x8f]) 149 | } 150 | 151 | #[test] 152 | fn decode_hex_invalid_length() { 153 | assert!(matches!(decode_hex("abc").unwrap_err(), DecodeHexError::InvalidLen(3))); 154 | } 155 | 156 | #[test] 157 | fn decode_hex_invalid_char() { 158 | assert!(matches!(decode_hex("x0").unwrap_err(), DecodeHexError::InvalidChar('x'))); 159 | assert!(matches!(decode_hex("0y").unwrap_err(), DecodeHexError::InvalidChar('y'))); 160 | } 161 | 162 | #[test] 163 | fn decode_hex_emtpy() { 164 | assert_eq!(decode_hex("").unwrap(), Vec::::new()); 165 | } 166 | 167 | quickcheck! { 168 | 169 | fn decode_hex_any_byte_lower(byte: u8) -> bool { 170 | decode_hex(&format!("{byte:02x}")).unwrap() == vec![byte] 171 | } 172 | 173 | fn decode_hex_any_byte_upper(byte: u8) -> bool { 174 | decode_hex(&format!("{byte:02X}")).unwrap() == vec![byte] 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /crates/rrg/src/session/fleetspeak.rs: -------------------------------------------------------------------------------- 1 | use log::{error, info}; 2 | 3 | /// A session implementation that uses real Fleetspeak connection. 4 | /// 5 | /// This is a normal session type that that is associated with some flow on the 6 | /// server. It keeps track of the responses it sends and collects statistics 7 | /// about network and runtime utilization to kill the action if it is needed. 8 | pub struct FleetspeakSession<'a> { 9 | /// Arguments passed to the agent. 10 | args: &'a crate::args::Args, 11 | /// A builder for responses sent through Fleetspeak to the GRR server. 12 | response_builder: crate::ResponseBuilder, 13 | /// Number of bytes sent since the session was created. 14 | network_bytes_sent: u64, 15 | /// Number of bytes we are allowed to send within the session. 16 | network_bytes_limit: Option, 17 | /// Time at which the session was created. 18 | real_time_start: std::time::Instant, 19 | /// Time which we are allowed to spend within the session. 20 | real_time_limit: Option, 21 | } 22 | 23 | impl<'a> FleetspeakSession<'a> { 24 | 25 | /// Dispatches the given `request` to an appropriate action handler. 26 | /// 27 | /// This is the main entry point of the session. It processes the request 28 | /// and sends the execution status back to the server. 29 | /// 30 | /// Note that the function accepts a `Result`. This is because we want to 31 | /// send the error (in case on occurred) back to the server. But this we can 32 | /// do only within a sesssion, so we have to create a session from a perhaps 33 | /// invalid request. 34 | /// 35 | /// Long-running actions spawned by requests that need to send heartbeat 36 | /// signal to Fleetspeak will do so with frequency not greater than the one 37 | /// specified the arguments passed to the agent. 38 | pub fn dispatch( 39 | args: &'a crate::args::Args, 40 | request: Result, 41 | ) { 42 | let request_id = match &request { 43 | Ok(request) => request.id(), 44 | Err(error) => match error.request_id() { 45 | Some(request_id) => request_id, 46 | None => { 47 | error!("invalid request: {}", error); 48 | return; 49 | } 50 | } 51 | }; 52 | 53 | info!("received request '{request_id}'"); 54 | 55 | let response_builder = crate::ResponseBuilder::new(request_id); 56 | 57 | let status = match request { 58 | Ok(mut request) => { 59 | let filters = request.take_filters(); 60 | let mut session = FleetspeakSession { 61 | args, 62 | response_builder: response_builder.with_filters(filters), 63 | network_bytes_sent: 0, 64 | network_bytes_limit: request.network_bytes_limit(), 65 | real_time_start: std::time::Instant::now(), 66 | real_time_limit: request.real_time_limit(), 67 | }; 68 | 69 | let result = crate::log::ResponseLogger::new(&request) 70 | .context(|| crate::action::dispatch(&mut session, request)); 71 | 72 | session.response_builder.status(result) 73 | }, 74 | Err(error) => { 75 | error!("invalid request '{request_id}': {error}"); 76 | response_builder.status(Err(error.into())) 77 | } 78 | }; 79 | 80 | status.send_unaccounted(); 81 | } 82 | } 83 | 84 | impl<'a> FleetspeakSession<'a> { 85 | 86 | /// Checks whether the network bytes limit was crossed. 87 | /// 88 | /// This function will return an error if it was. 89 | fn check_network_bytes_limit(&self) -> crate::session::Result<()> { 90 | use crate::session::error::NetworkBytesLimitExceededError; 91 | 92 | if let Some(network_bytes_limit) = self.network_bytes_limit { 93 | if self.network_bytes_sent > network_bytes_limit { 94 | return Err(NetworkBytesLimitExceededError { 95 | network_bytes_sent: self.network_bytes_sent, 96 | network_bytes_limit, 97 | }.into()); 98 | } 99 | } 100 | 101 | Ok(()) 102 | } 103 | 104 | /// Checks whether the real (wall) time limit was crossed. 105 | /// 106 | /// This function will return an error if it was. 107 | fn check_real_time_limit(&self) -> crate::session::Result<()> { 108 | use crate::session::error::RealTimeLimitExceededError; 109 | 110 | if let Some(real_time_limit) = self.real_time_limit { 111 | let real_time_spent = self.real_time_start.elapsed(); 112 | if real_time_spent > real_time_limit { 113 | return Err(RealTimeLimitExceededError { 114 | real_time_spent, 115 | real_time_limit, 116 | }.into()); 117 | } 118 | } 119 | 120 | Ok(()) 121 | } 122 | } 123 | 124 | impl<'a> crate::session::Session for FleetspeakSession<'a> { 125 | 126 | fn args(&self) -> &crate::args::Args { 127 | self.args 128 | } 129 | 130 | fn reply(&mut self, item: I) -> crate::session::Result<()> 131 | where 132 | I: crate::response::Item, 133 | { 134 | let item = crate::response::PreparedItem::from(item); 135 | 136 | use crate::response::FilteredReply::*; 137 | let reply = match self.response_builder.reply(item) { 138 | Accepted(reply) => reply, 139 | Rejected => return Ok(()), 140 | Error(error) => return Err(error.into()), 141 | }; 142 | 143 | self.network_bytes_sent += reply.send_unaccounted() as u64; 144 | self.check_network_bytes_limit()?; 145 | 146 | // TODO(@panhania): Enforce CPU time limits. 147 | self.check_real_time_limit()?; 148 | 149 | Ok(()) 150 | } 151 | 152 | fn send(&mut self, sink: crate::Sink, item: I) -> crate::session::Result<()> 153 | where 154 | I: crate::response::Item, 155 | { 156 | let parcel = crate::response::Parcel::new(sink, item); 157 | 158 | self.network_bytes_sent += parcel.send_unaccounted() as u64; 159 | self.check_network_bytes_limit()?; 160 | 161 | // TODO(@panhania): Enforce CPU time limits. 162 | self.check_real_time_limit()?; 163 | 164 | Ok(()) 165 | } 166 | 167 | fn heartbeat(&mut self) { 168 | fleetspeak::heartbeat_with_throttle(self.args.heartbeat_rate); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /crates/rrg/src/action.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | //! Handlers and types for agent's actions. 7 | //! 8 | //! The basic functionality that a GRR agent exposes is called an _action_. 9 | //! Actions are invoked by the server (when running a _flow_), should gather 10 | //! requested information and report back to the server. 11 | //! 12 | //! In RRG each action consists of three components: a request type, a response 13 | //! type and an action handler. Request and response types wrap lower-level 14 | //! Protocol Buffer messages sent by and to the GRR server. Handlers accept one 15 | //! instance of the corresponding request type and send some (zero or more) 16 | //! instances of the corresponding response type. 17 | 18 | #[cfg(feature = "action-get_system_metadata")] 19 | pub mod get_system_metadata; 20 | 21 | #[cfg(feature = "action-get_file_metadata")] 22 | pub mod get_file_metadata; 23 | 24 | #[cfg(feature = "action-get_file_contents")] 25 | pub mod get_file_contents; 26 | 27 | #[cfg(feature = "action-get_file_contents_kmx")] 28 | pub mod get_file_contents_kmx; 29 | 30 | #[cfg(feature = "action-get_file_sha256")] 31 | pub mod get_file_sha256; 32 | 33 | #[cfg(feature = "action-grep_file_contents")] 34 | pub mod grep_file_contents; 35 | 36 | #[cfg(feature = "action-get_filesystem_timeline")] 37 | pub mod get_filesystem_timeline; 38 | 39 | #[cfg(feature = "action-get_filesystem_timeline_tsk")] 40 | pub mod get_filesystem_timeline_tsk; 41 | 42 | #[cfg(feature = "action-get_tcp_response")] 43 | pub mod get_tcp_response; 44 | 45 | #[cfg(feature = "action-list_connections")] 46 | pub mod list_connections; 47 | 48 | #[cfg(feature = "action-list_interfaces")] 49 | pub mod list_interfaces; 50 | 51 | #[cfg(feature = "action-list_mounts")] 52 | pub mod list_mounts; 53 | 54 | #[cfg(feature = "action-list_utmp_users")] 55 | pub mod list_utmp_users; 56 | 57 | #[cfg(feature = "action-get_winreg_value")] 58 | pub mod get_winreg_value; 59 | 60 | #[cfg(feature = "action-list_winreg_values")] 61 | pub mod list_winreg_values; 62 | 63 | #[cfg(feature = "action-list_winreg_keys")] 64 | pub mod list_winreg_keys; 65 | 66 | #[cfg(feature = "action-query_wmi")] 67 | pub mod query_wmi; 68 | 69 | #[cfg(feature = "action-execute_signed_command")] 70 | pub mod execute_signed_command; 71 | 72 | #[cfg(feature = "action-dump_process_memory")] 73 | pub mod dump_process_memory; 74 | 75 | #[cfg(feature = "action-scan_memory_yara")] 76 | pub mod scan_memory_yara; 77 | 78 | use log::info; 79 | 80 | /// Dispatches the given `request` to an appropriate action handler. 81 | /// 82 | /// This method is a mapping between action names (as specified in the protocol) 83 | /// and action handlers (implemented on the agent). 84 | /// 85 | /// # Errors 86 | /// 87 | /// This function will return an error if the given action is unknown (or not 88 | /// yet implemented). 89 | /// 90 | /// It will also error out if the action execution itself fails for whatever 91 | /// reason. 92 | pub fn dispatch<'s, S>(session: &mut S, request: crate::Request) -> Result<(), crate::session::Error> 93 | where 94 | S: crate::session::Session, 95 | { 96 | use crate::request::Action::*; 97 | 98 | let request_id = request.id(); 99 | let action = request.action(); 100 | 101 | info!("dispatching request '{request_id}': {action}"); 102 | 103 | let result = match request.action() { 104 | #[cfg(feature = "action-get_system_metadata")] 105 | GetSystemMetadata => { 106 | handle(session, request, self::get_system_metadata::handle) 107 | } 108 | #[cfg(feature = "action-get_file_metadata")] 109 | GetFileMetadata => { 110 | handle(session, request, self::get_file_metadata::handle) 111 | } 112 | #[cfg(feature = "action-get_file_contents")] 113 | GetFileContents => { 114 | handle(session, request, self::get_file_contents::handle) 115 | } 116 | #[cfg(feature = "action-get_file_contents_kmx")] 117 | GetFileContentsKmx => { 118 | handle(session, request, self::get_file_contents_kmx::handle) 119 | } 120 | #[cfg(feature = "action-get_file_sha256")] 121 | GetFileSha256 => { 122 | handle(session, request, self::get_file_sha256::handle) 123 | } 124 | #[cfg(feature = "action-grep_file_contents")] 125 | GrepFileContents => { 126 | handle(session, request, self::grep_file_contents::handle) 127 | } 128 | #[cfg(feature = "action-get_filesystem_timeline")] 129 | GetFilesystemTimeline => { 130 | handle(session, request, self::get_filesystem_timeline::handle) 131 | } 132 | #[cfg(feature = "action-get_filesystem_timeline_tsk")] 133 | GetFilesystemTimelineTsk => { 134 | handle(session, request, self::get_filesystem_timeline_tsk::handle) 135 | } 136 | #[cfg(feature = "action-get_tcp_response")] 137 | GetTcpResponse => { 138 | handle(session, request, self::get_tcp_response::handle) 139 | } 140 | #[cfg(feature = "action-list_connections")] 141 | ListConnections => { 142 | handle(session, request, self::list_connections::handle) 143 | } 144 | #[cfg(feature = "action-list_interfaces")] 145 | ListInterfaces => { 146 | handle(session, request, self::list_interfaces::handle) 147 | } 148 | #[cfg(feature = "action-list_mounts")] 149 | ListMounts => { 150 | handle(session, request, self::list_mounts::handle) 151 | } 152 | #[cfg(feature = "action-list_utmp_users")] 153 | ListUtmpUsers => { 154 | handle(session, request, self::list_utmp_users::handle) 155 | } 156 | #[cfg(feature = "action-get_winreg_value")] 157 | GetWinregValue => { 158 | handle(session, request, self::get_winreg_value::handle) 159 | } 160 | #[cfg(feature = "action-list_winreg_values")] 161 | ListWinregValues => { 162 | handle(session, request, self::list_winreg_values::handle) 163 | } 164 | #[cfg(feature = "action-list_winreg_keys")] 165 | ListWinregKeys => { 166 | handle(session, request, self::list_winreg_keys::handle) 167 | } 168 | #[cfg(feature = "action-query_wmi")] 169 | QueryWmi => { 170 | handle(session, request, self::query_wmi::handle) 171 | } 172 | #[cfg(feature = "action-execute_signed_command")] 173 | ExecuteSignedCommand => { 174 | handle(session, request, self::execute_signed_command::handle) 175 | } 176 | #[cfg(feature = "action-dump_process_memory")] 177 | DumpProcessMemory => { 178 | handle(session, request, self::dump_process_memory::handle) 179 | } 180 | #[cfg(feature = "action-scan_memory_yara")] 181 | ScanProcessMemoryYara => { 182 | handle(session, request, self::scan_memory_yara::handle) 183 | } 184 | // We allow `unreachable_patterns` because otherwise we get a warning if 185 | // we compile with all the actions enabled. 186 | #[allow(unreachable_patterns)] 187 | action => { 188 | return Err(crate::session::Error::unsupported_action(action)); 189 | } 190 | }; 191 | 192 | info!("finished dispatching request '{request_id}'"); 193 | 194 | result 195 | } 196 | 197 | /// Handles a `request` using the specified `handler`. 198 | /// 199 | /// This method will attempt to interpret request arguments for the specific 200 | /// action and execute the handler with them. 201 | /// 202 | /// # Errors 203 | /// 204 | /// This function will return an error if the request arguments cannot be parsed 205 | /// for the specific action or if the action execution fails. 206 | fn handle(session: &mut S, request: crate::Request, handler: H) -> crate::session::Result<()> 207 | where 208 | S: crate::session::Session, 209 | A: crate::request::Args, 210 | H: FnOnce(&mut S, A) -> crate::session::Result<()>, 211 | { 212 | Ok(handler(session, request.args()?)?) 213 | } 214 | -------------------------------------------------------------------------------- /crates/rrg/src/session/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | /// An error type for failures that can occur during a session. 7 | #[derive(Debug)] 8 | pub struct Error { 9 | /// A corresponding [`ErrorKind`] of this error. 10 | kind: ErrorKind, 11 | /// A detailed error object. 12 | error: Box, 13 | } 14 | 15 | /// Kinds of errors that can happen during a session. 16 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 17 | pub enum ErrorKind { 18 | /// The action request was invalid. 19 | InvalidRequest(crate::request::ParseRequestErrorKind), 20 | /// The requested action is not supported. 21 | UnsupportedAction, 22 | /// The arguments given for the action were malformed. 23 | InvalidArgs, 24 | /// The action execution failed. 25 | ActionFailure, 26 | /// Filter evaluation on action result failed. 27 | FilterFailure, 28 | /// Action execution crossed the allowed network bytes limit. 29 | NetworkBytesLimitExceeded, 30 | /// Action execution crossed the allowed real (wall) time limit. 31 | RealTimeLimitExceeded, 32 | } 33 | 34 | impl Error { 35 | 36 | /// Converts an arbitrary action-issued error to a session error. 37 | /// 38 | /// This function should be used to construct session errors from action 39 | /// specific error types and propagate them further in the session pipeline. 40 | pub fn action(error: E) -> Error 41 | where 42 | E: std::error::Error + 'static, 43 | { 44 | Error { 45 | kind: ErrorKind::ActionFailure, 46 | error: Box::new(error), 47 | } 48 | } 49 | 50 | /// Converts an action that is not supported to a session error. 51 | pub fn unsupported_action(action: crate::request::Action) -> Error { 52 | Error { 53 | kind: ErrorKind::UnsupportedAction, 54 | error: Box::new(UnsupportedActionError { action }), 55 | } 56 | } 57 | 58 | /// Returns the corresponding [`ErrorKind`] of this error. 59 | pub fn kind(&self) -> ErrorKind { 60 | self.kind 61 | } 62 | } 63 | 64 | impl std::fmt::Display for Error { 65 | 66 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 67 | use ErrorKind::*; 68 | 69 | match self.kind { 70 | InvalidRequest(_) => { 71 | // `self.error` is an instance of `ParseRequestError` which 72 | // contains meaningful message, we don't need to provide it 73 | // ourselves here. 74 | write!(fmt, "{}", self.error) 75 | } 76 | UnsupportedAction => { 77 | // Same as with `InvalidRequest` variant, the `self.error` is an 78 | // instance of `UnsupportedActionError` and has enough details. 79 | write!(fmt, "{}", self.error) 80 | } 81 | InvalidArgs => { 82 | write!(fmt, "invalid action arguments: {}", self.error) 83 | } 84 | ActionFailure => { 85 | write!(fmt, "action execution failed: {}", self.error) 86 | } 87 | FilterFailure => { 88 | write!(fmt, "filter evaluation failed: {}", self.error) 89 | } 90 | NetworkBytesLimitExceeded => { 91 | write!(fmt, "network bytes limit exceeded: {}", self.error) 92 | } 93 | RealTimeLimitExceeded => { 94 | write!(fmt, "real time limit exceeded: {}", self.error) 95 | } 96 | } 97 | } 98 | } 99 | 100 | impl std::error::Error for Error { 101 | 102 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 103 | Some(self.error.as_ref()) 104 | } 105 | } 106 | 107 | impl From for Error { 108 | 109 | fn from(error: crate::request::ParseRequestError) -> Error { 110 | Error { 111 | kind: ErrorKind::InvalidRequest(error.kind()), 112 | error: Box::new(error), 113 | } 114 | } 115 | } 116 | 117 | impl From for Error { 118 | 119 | fn from(error: crate::request::ParseArgsError) -> Error { 120 | Error { 121 | kind: ErrorKind::InvalidArgs, 122 | error: Box::new(error), 123 | } 124 | } 125 | } 126 | 127 | impl From for Error { 128 | 129 | fn from(error: crate::filter::Error) -> Error { 130 | Error { 131 | kind: ErrorKind::FilterFailure, 132 | error: Box::new(error), 133 | } 134 | } 135 | } 136 | 137 | impl From for rrg_proto::rrg::status::Error { 138 | 139 | fn from(error: Error) -> rrg_proto::rrg::status::Error { 140 | let mut proto = rrg_proto::rrg::status::Error::new(); 141 | proto.set_type(error.kind.into()); 142 | proto.set_message(error.to_string()); 143 | 144 | proto 145 | } 146 | } 147 | 148 | impl From for rrg_proto::rrg::status::error::Type { 149 | 150 | fn from(kind: ErrorKind) -> rrg_proto::rrg::status::error::Type { 151 | use ErrorKind::*; 152 | 153 | match kind { 154 | InvalidRequest(kind) => kind.into(), 155 | UnsupportedAction => Self::UNSUPPORTED_ACTION, 156 | InvalidArgs => Self::INVALID_ARGS, 157 | ActionFailure => Self::ACTION_FAILURE, 158 | FilterFailure => Self::FILTER_FAILURE, 159 | NetworkBytesLimitExceeded => Self::NETWORK_BYTES_SENT_LIMIT_EXCEEDED, 160 | RealTimeLimitExceeded => Self::REAL_TIME_LIMIT_EXCEEDED, 161 | } 162 | } 163 | } 164 | 165 | /// An error type for when the action specified in the request is not supported. 166 | #[derive(Debug)] 167 | struct UnsupportedActionError { 168 | action: crate::request::Action, 169 | } 170 | 171 | impl std::fmt::Display for UnsupportedActionError { 172 | 173 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 174 | write!(fmt, "unsupported action '{}'", self.action) 175 | } 176 | } 177 | 178 | impl std::error::Error for UnsupportedActionError { 179 | } 180 | 181 | /// An error type raised when the network bytes limit has been exceeded. 182 | #[derive(Debug)] 183 | pub struct NetworkBytesLimitExceededError { 184 | /// Number of bytes we actually sent. 185 | pub network_bytes_sent: u64, 186 | /// Number of bytes we were allowed to send. 187 | pub network_bytes_limit: u64, 188 | } 189 | 190 | impl std::fmt::Display for NetworkBytesLimitExceededError { 191 | 192 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 193 | write! { 194 | fmt, 195 | "sent {} bytes out of allowed {}", 196 | self.network_bytes_sent, 197 | self.network_bytes_limit, 198 | } 199 | } 200 | } 201 | 202 | impl std::error::Error for NetworkBytesLimitExceededError { 203 | } 204 | 205 | impl From for Error { 206 | 207 | fn from(error: NetworkBytesLimitExceededError) -> Error { 208 | Error { 209 | kind: ErrorKind::NetworkBytesLimitExceeded, 210 | error: Box::new(error), 211 | } 212 | } 213 | } 214 | 215 | /// An error type raised when the real (wall) time limit has been exceeded. 216 | #[derive(Debug)] 217 | pub struct RealTimeLimitExceededError { 218 | /// Amount of real time we actually spent on executing the action. 219 | pub real_time_spent: std::time::Duration, 220 | /// Amount of real time we were allowed to spend on executing the action. 221 | pub real_time_limit: std::time::Duration, 222 | } 223 | 224 | impl std::fmt::Display for RealTimeLimitExceededError { 225 | 226 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 227 | write! { 228 | fmt, 229 | "spent real time {} out of allowed {}", 230 | humantime::format_duration(self.real_time_spent), 231 | humantime::format_duration(self.real_time_limit), 232 | } 233 | } 234 | } 235 | 236 | impl std::error::Error for RealTimeLimitExceededError { 237 | } 238 | 239 | impl From for Error { 240 | 241 | fn from(error: RealTimeLimitExceededError) -> Error { 242 | Error { 243 | kind: ErrorKind::RealTimeLimitExceeded, 244 | error: Box::new(error), 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /crates/rrg/src/action/get_file_sha256.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | use std::path::PathBuf; 6 | 7 | /// Arguments of the `get_file_sha256` action. 8 | pub struct Args { 9 | /// Absolute path to the file to get the SHA-256 hash of. 10 | path: PathBuf, 11 | /// Byte offset from which the content should be hashed. 12 | offset: u64, 13 | /// Number of bytes to hash (from the start offset). 14 | len: Option>, 15 | } 16 | 17 | /// Result of the `get_file_sha256` action. 18 | struct Item { 19 | /// Absolute path of the file this result corresponds to. 20 | path: PathBuf, 21 | /// Byte offset from which the file content was hashed. 22 | offset: u64, 23 | /// Number of bytes of the file used to produce the hash. 24 | len: u64, 25 | /// SHA-256 hash digest of the file content. 26 | sha256: [u8; 32], 27 | } 28 | 29 | /// Handle invocations of the `get_file_sha256` action. 30 | pub fn handle(session: &mut S, args: Args) -> crate::session::Result<()> 31 | where 32 | S: crate::session::Session, 33 | { 34 | use std::io::{BufRead as _, Read as _, Seek as _}; 35 | 36 | let file = std::fs::File::open(&args.path) 37 | .map_err(crate::session::Error::action)?; 38 | let mut file = std::io::BufReader::new(file); 39 | 40 | file.seek(std::io::SeekFrom::Start(args.offset)) 41 | .map_err(crate::session::Error::action)?; 42 | 43 | let mut file = file.take(match args.len { 44 | Some(len) => u64::from(len), 45 | None => u64::MAX, 46 | }); 47 | 48 | use sha2::Digest as _; 49 | let mut sha256 = sha2::Sha256::new(); 50 | loop { 51 | let buf = match file.fill_buf() { 52 | Ok(buf) if buf.is_empty() => break, 53 | Ok(buf) => buf, 54 | Err(error) => return Err(crate::session::Error::action(error)), 55 | }; 56 | sha256.update(&buf[..]); 57 | 58 | let buf_len = buf.len(); 59 | file.consume(buf_len); 60 | } 61 | let sha256 = <[u8; 32]>::from(sha256.finalize()); 62 | 63 | let len = file.stream_position() 64 | .map_err(crate::session::Error::action)?; 65 | 66 | session.reply(Item { 67 | path: args.path, 68 | offset: args.offset, 69 | len, 70 | sha256, 71 | })?; 72 | 73 | Ok(()) 74 | } 75 | 76 | impl crate::request::Args for Args { 77 | 78 | type Proto = rrg_proto::get_file_sha256::Args; 79 | 80 | fn from_proto(mut proto: Self::Proto) -> Result { 81 | use crate::request::ParseArgsError; 82 | 83 | let path = PathBuf::try_from(proto.take_path()) 84 | .map_err(|error| ParseArgsError::invalid_field("path", error))?; 85 | 86 | Ok(Args { 87 | path, 88 | offset: proto.offset(), 89 | len: std::num::NonZero::new(proto.length()), 90 | }) 91 | } 92 | } 93 | 94 | impl crate::response::Item for Item { 95 | 96 | type Proto = rrg_proto::get_file_sha256::Result; 97 | 98 | fn into_proto(self) -> Self::Proto { 99 | let mut proto = rrg_proto::get_file_sha256::Result::new(); 100 | proto.set_path(self.path.into()); 101 | proto.set_offset(self.offset); 102 | proto.set_length(self.len); 103 | proto.set_sha256(self.sha256.to_vec()); 104 | 105 | proto 106 | } 107 | } 108 | 109 | #[cfg(test)] 110 | mod tests { 111 | 112 | use super::*; 113 | 114 | #[test] 115 | fn handle_default() { 116 | let mut tempfile = tempfile::NamedTempFile::new() 117 | .unwrap(); 118 | 119 | use std::io::Write as _; 120 | tempfile.as_file_mut().write_all(b"hello\n") 121 | .unwrap(); 122 | 123 | let args = Args { 124 | path: tempfile.path().to_path_buf(), 125 | offset: 0, 126 | len: None, 127 | }; 128 | 129 | let mut session = crate::session::FakeSession::new(); 130 | assert!(handle(&mut session, args).is_ok()); 131 | 132 | assert_eq!(session.reply_count(), 1); 133 | 134 | let item = session.reply::(0); 135 | assert_eq!(item.path, tempfile.path()); 136 | assert_eq!(item.offset, 0); 137 | assert_eq!(item.len, u64::try_from(b"hello\n".len()).unwrap()); 138 | assert_eq!(item.sha256, [ 139 | // Pre-computed by the `sha256sum` tool. 140 | 0x58, 0x91, 0xb5, 0xb5, 0x22, 0xd5, 0xdf, 0x08, 141 | 0x6d, 0x0f, 0xf0, 0xb1, 0x10, 0xfb, 0xd9, 0xd2, 142 | 0x1b, 0xb4, 0xfc, 0x71, 0x63, 0xaf, 0x34, 0xd0, 143 | 0x82, 0x86, 0xa2, 0xe8, 0x46, 0xf6, 0xbe, 0x03, 144 | ]); 145 | } 146 | 147 | #[test] 148 | fn handle_offset() { 149 | let mut tempfile = tempfile::NamedTempFile::new() 150 | .unwrap(); 151 | 152 | use std::io::Write as _; 153 | tempfile.as_file_mut().write_all(b"hello\n") 154 | .unwrap(); 155 | 156 | let args = Args { 157 | path: tempfile.path().to_path_buf(), 158 | offset: u64::try_from("".len()).unwrap(), 159 | len: None, 160 | }; 161 | 162 | let mut session = crate::session::FakeSession::new(); 163 | assert!(handle(&mut session, args).is_ok()); 164 | 165 | assert_eq!(session.reply_count(), 1); 166 | 167 | let item = session.reply::(0); 168 | assert_eq!(item.path, tempfile.path()); 169 | assert_eq!(item.offset, u64::try_from(b"".len()).unwrap()); 170 | assert_eq!(item.len, u64::try_from(b"hello\n".len()).unwrap()); 171 | assert_eq!(item.sha256, [ 172 | // Pre-computed by the `sha256sum` tool. 173 | 0x58, 0x91, 0xb5, 0xb5, 0x22, 0xd5, 0xdf, 0x08, 174 | 0x6d, 0x0f, 0xf0, 0xb1, 0x10, 0xfb, 0xd9, 0xd2, 175 | 0x1b, 0xb4, 0xfc, 0x71, 0x63, 0xaf, 0x34, 0xd0, 176 | 0x82, 0x86, 0xa2, 0xe8, 0x46, 0xf6, 0xbe, 0x03, 177 | ]); 178 | } 179 | 180 | #[test] 181 | fn handle_len() { 182 | let mut tempfile = tempfile::NamedTempFile::new() 183 | .unwrap(); 184 | 185 | use std::io::Write as _; 186 | tempfile.as_file_mut().write_all(b"hello\n") 187 | .unwrap(); 188 | 189 | let args = Args { 190 | path: tempfile.path().to_path_buf(), 191 | offset: 0, 192 | len: std::num::NonZero::new(b"hello\n".len().try_into().unwrap()), 193 | }; 194 | 195 | let mut session = crate::session::FakeSession::new(); 196 | assert!(handle(&mut session, args).is_ok()); 197 | 198 | assert_eq!(session.reply_count(), 1); 199 | 200 | let item = session.reply::(0); 201 | assert_eq!(item.path, tempfile.path()); 202 | assert_eq!(item.offset, 0); 203 | assert_eq!(item.len, u64::try_from(b"hello\n".len()).unwrap()); 204 | assert_eq!(item.sha256, [ 205 | // Pre-computed by the `sha256sum` tool. 206 | 0x58, 0x91, 0xb5, 0xb5, 0x22, 0xd5, 0xdf, 0x08, 207 | 0x6d, 0x0f, 0xf0, 0xb1, 0x10, 0xfb, 0xd9, 0xd2, 208 | 0x1b, 0xb4, 0xfc, 0x71, 0x63, 0xaf, 0x34, 0xd0, 209 | 0x82, 0x86, 0xa2, 0xe8, 0x46, 0xf6, 0xbe, 0x03, 210 | ]); 211 | } 212 | 213 | #[test] 214 | fn handle_large() { 215 | let mut tempfile = tempfile::NamedTempFile::new() 216 | .unwrap(); 217 | 218 | use std::io::Read as _; 219 | std::io::copy(&mut std::io::repeat(0).take(13371337), &mut tempfile) 220 | .unwrap(); 221 | 222 | let args = Args { 223 | path: tempfile.path().to_path_buf(), 224 | offset: 0, 225 | len: None, 226 | }; 227 | 228 | let mut session = crate::session::FakeSession::new(); 229 | assert!(handle(&mut session, args).is_ok()); 230 | 231 | assert_eq!(session.reply_count(), 1); 232 | 233 | let item = session.reply::(0); 234 | assert_eq!(item.path, tempfile.path()); 235 | assert_eq!(item.offset, 0); 236 | assert_eq!(item.len, 13371337); 237 | assert_eq!(item.sha256, [ 238 | // Pre-computed by `head --bytes=13371337 < /dev/zero | sha256sum`. 239 | 0xda, 0xa6, 0x04, 0x11, 0x35, 0x03, 0xdb, 0x38, 240 | 0xe3, 0x62, 0xfe, 0xff, 0x8f, 0x73, 0xc1, 0xf9, 241 | 0xb2, 0x6f, 0x02, 0x85, 0x3d, 0x2f, 0x47, 0x8d, 242 | 0x52, 0x16, 0xc5, 0x70, 0x32, 0x54, 0x1c, 0xf8, 243 | ]); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /crates/rrg/src/session.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | //! Utilities for working with sessions. 7 | //! 8 | //! Sessions are created per action request and should be used as the only way 9 | //! to communicate with the server. Sessions are responsible for handling action 10 | //! errors (when something goes wrong) or notifying the server that the action 11 | //! finished (by sending appropriate status signal). 12 | //! 13 | //! They also keep track of various statistics (such as number of transferred 14 | //! bytes, action runtime, etc.) and stop the execution if they exceed limits 15 | //! for a particular request. 16 | 17 | mod error; 18 | 19 | #[cfg(test)] 20 | mod fake; 21 | mod fleetspeak; 22 | 23 | #[cfg(test)] 24 | pub use crate::session::fake::FakeSession; 25 | pub use crate::session::fleetspeak::FleetspeakSession; 26 | 27 | pub use self::error::{Error}; 28 | 29 | /// A specialized `Result` type for sessions. 30 | pub type Result = std::result::Result; 31 | 32 | /// Abstraction for various kinds of sessions. 33 | pub trait Session { 34 | 35 | /// Provides the arguments passed to the agent. 36 | fn args(&self) -> &crate::args::Args; 37 | 38 | /// Sends a reply to the flow that call the action. 39 | fn reply(&mut self, item: I) -> Result<()> 40 | where I: crate::response::Item + 'static; 41 | 42 | /// Sends an item to a particular sink. 43 | fn send(&mut self, sink: crate::Sink, item: I) -> Result<()> 44 | where I: crate::response::Item + 'static; 45 | 46 | /// Sends a heartbeat signal to the Fleetspeak process. 47 | fn heartbeat(&mut self); 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | 53 | use super::*; 54 | use crate::Sink; 55 | 56 | #[test] 57 | fn test_fake_reply_count() { 58 | 59 | fn handle(session: &mut S, _: ()) { 60 | session.reply(()).unwrap(); 61 | session.reply(()).unwrap(); 62 | session.reply(()).unwrap(); 63 | } 64 | 65 | let mut session = FakeSession::new(); 66 | handle(&mut session, ()); 67 | 68 | assert_eq!(session.reply_count(), 3); 69 | } 70 | 71 | #[test] 72 | fn test_fake_parcel_count() { 73 | 74 | fn handle(session: &mut S, _: ()) { 75 | session.send(Sink::Startup, ()).unwrap(); 76 | session.send(Sink::Blob, ()).unwrap(); 77 | session.send(Sink::Startup, ()).unwrap(); 78 | } 79 | 80 | let mut session = FakeSession::new(); 81 | handle(&mut session, ()); 82 | 83 | assert_eq!(session.parcel_count(Sink::Blob), 1); 84 | assert_eq!(session.parcel_count(Sink::Startup), 2); 85 | } 86 | 87 | #[test] 88 | fn test_fake_reply_correct_response() { 89 | 90 | fn handle(session: &mut S, _: ()) { 91 | session.reply(StringResponse::from("foo")).unwrap(); 92 | session.reply(StringResponse::from("bar")).unwrap(); 93 | } 94 | 95 | let mut session = FakeSession::new(); 96 | handle(&mut session, ()); 97 | 98 | assert_eq!(session.reply::(0).0, "foo"); 99 | assert_eq!(session.reply::(1).0, "bar"); 100 | } 101 | 102 | #[test] 103 | #[should_panic(expected = "no reply #0")] 104 | fn test_fake_reply_incorrect_response_id() { 105 | 106 | fn handle(_: &mut S, _: ()) { 107 | } 108 | 109 | let mut session = FakeSession::new(); 110 | handle(&mut session, ()); 111 | 112 | session.reply::<()>(0); 113 | } 114 | 115 | #[test] 116 | #[should_panic(expected = "unexpected reply type")] 117 | fn test_fake_reply_incorrect_response_type() { 118 | 119 | fn handle(session: &mut S, _: ()) { 120 | session.reply(StringResponse::from("quux")).unwrap(); 121 | } 122 | 123 | let mut session = FakeSession::new(); 124 | handle(&mut session, ()); 125 | 126 | session.reply::<()>(0); 127 | } 128 | 129 | #[test] 130 | fn test_fake_parcel_correct_parcel() { 131 | 132 | fn handle(session: &mut S, _: ()) { 133 | session.send(Sink::Startup, StringResponse::from("foo")).unwrap(); 134 | session.send(Sink::Startup, StringResponse::from("bar")).unwrap(); 135 | } 136 | 137 | let mut session = FakeSession::new(); 138 | handle(&mut session, ()); 139 | 140 | let response_foo = session.parcel::(Sink::Startup, 0); 141 | let response_bar = session.parcel::(Sink::Startup, 1); 142 | assert_eq!(response_foo.0, "foo"); 143 | assert_eq!(response_bar.0, "bar"); 144 | } 145 | 146 | #[test] 147 | #[should_panic(expected = "no parcel #42")] 148 | fn test_fake_parcel_incorrect_parcel_id() { 149 | 150 | fn handle(session: &mut S, _: ()) { 151 | session.send(Sink::Startup, ()).unwrap(); 152 | session.send(Sink::Startup, ()).unwrap(); 153 | } 154 | 155 | let mut session = FakeSession::new(); 156 | handle(&mut session, ()); 157 | 158 | session.parcel::<()>(Sink::Startup, 42); 159 | } 160 | 161 | #[test] 162 | #[should_panic(expected = "unexpected parcel type")] 163 | fn test_fake_parcel_incorrect_parcel_type() { 164 | 165 | fn handle(session: &mut S, _: ()) { 166 | session.send(Sink::Startup, StringResponse::from("quux")).unwrap(); 167 | } 168 | 169 | let mut session = FakeSession::new(); 170 | handle(&mut session, ()); 171 | 172 | session.parcel::<()>(Sink::Startup, 0); 173 | } 174 | 175 | #[test] 176 | fn test_fake_replies_no_parcels() { 177 | 178 | fn handle(_: &mut S, _: ()) { 179 | } 180 | 181 | let mut session = FakeSession::new(); 182 | handle(&mut session, ()); 183 | 184 | let mut replies = session.replies::<()>(); 185 | assert_eq!(replies.next(), None); 186 | } 187 | 188 | #[test] 189 | fn test_fake_replies_multiple_parcels() { 190 | 191 | fn handle(session: &mut S, _: ()) { 192 | session.reply(StringResponse::from("foo")).unwrap(); 193 | session.reply(StringResponse::from("bar")).unwrap(); 194 | session.reply(StringResponse::from("baz")).unwrap(); 195 | } 196 | 197 | let mut session = FakeSession::new(); 198 | handle(&mut session, ()); 199 | 200 | let mut replies = session.replies::(); 201 | assert_eq!(replies.next().unwrap().0, "foo"); 202 | assert_eq!(replies.next().unwrap().0, "bar"); 203 | assert_eq!(replies.next().unwrap().0, "baz"); 204 | assert_eq!(replies.next(), None); 205 | } 206 | 207 | #[test] 208 | fn test_fake_parcels_no_parcels() { 209 | 210 | fn handle(_: &mut S, _: ()) { 211 | } 212 | 213 | let mut session = FakeSession::new(); 214 | handle(&mut session, ()); 215 | 216 | let mut parcels = session.parcels::<()>(Sink::Startup); 217 | assert_eq!(parcels.next(), None); 218 | } 219 | 220 | #[test] 221 | fn test_fake_parcels_multiple_parcels() { 222 | 223 | fn handle(session: &mut S, _: ()) { 224 | session.send(Sink::Startup, StringResponse::from("foo")).unwrap(); 225 | session.send(Sink::Startup, StringResponse::from("bar")).unwrap(); 226 | session.send(Sink::Startup, StringResponse::from("baz")).unwrap(); 227 | } 228 | 229 | let mut session = FakeSession::new(); 230 | handle(&mut session, ()); 231 | 232 | let mut parcels = session.parcels::(Sink::Startup); 233 | assert_eq!(parcels.next().unwrap().0, "foo"); 234 | assert_eq!(parcels.next().unwrap().0, "bar"); 235 | assert_eq!(parcels.next().unwrap().0, "baz"); 236 | assert_eq!(parcels.next(), None); 237 | } 238 | 239 | #[derive(Debug, PartialEq, Eq)] 240 | struct StringResponse(String); 241 | 242 | impl> From for StringResponse { 243 | 244 | fn from(string: S) -> StringResponse { 245 | StringResponse(string.into()) 246 | } 247 | } 248 | 249 | impl crate::response::Item for StringResponse { 250 | 251 | type Proto = protobuf::well_known_types::wrappers::StringValue; 252 | 253 | fn into_proto(self) -> protobuf::well_known_types::wrappers::StringValue { 254 | let mut proto = protobuf::well_known_types::wrappers::StringValue::new(); 255 | proto.value = self.0; 256 | 257 | proto 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /crates/rrg/src/action/list_utmp_users.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | /// Arguments of the `list_utmp_users` action. 7 | #[cfg(target_os = "linux")] 8 | pub struct Args { 9 | /// Path to the file to use as a source for `utmp` records. 10 | /// 11 | /// Typically this should be `/var/log/wtmp`. 12 | path: std::path::PathBuf, 13 | } 14 | 15 | /// Result of the `list_utmp_users` action. 16 | #[cfg(target_os = "linux")] 17 | pub struct Item { 18 | /// Name of an individual user retrieved from `utmp` records. 19 | username: std::ffi::OsString, 20 | } 21 | 22 | /// Handles invocations of the `list_utmp_users` action. 23 | #[cfg(target_os = "linux")] 24 | pub fn handle(session: &mut S, args: Args) -> crate::session::Result<()> 25 | where 26 | S: crate::session::Session, 27 | { 28 | let mut file = std::fs::File::open(&args.path) 29 | .map_err(crate::session::Error::action)?; 30 | 31 | let mut usernames = std::collections::HashSet::new(); 32 | 33 | loop { 34 | use std::io::Read as _; 35 | 36 | let mut buf = [0u8; std::mem::size_of::()]; 37 | match file.read_exact(&mut buf) { 38 | Ok(()) => (), 39 | Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => { 40 | break; 41 | } 42 | Err(error) => { 43 | return Err(crate::session::Error::action(error)); 44 | } 45 | } 46 | 47 | const TYPE_OFFSET: usize = std::mem::offset_of!(libc::utmpx, ut_type); 48 | const TYPE_SIZE: usize = std::mem::size_of::(); 49 | 50 | let type_bytes = <_>::try_from(&buf[TYPE_OFFSET..TYPE_OFFSET + TYPE_SIZE]) 51 | .expect("invalid type of utmp"); 52 | // According to [1], `utmp` uses litle-endian byte order. However, it 53 | // might be the case that it actually uses native-endian. This should be 54 | // verified and adjusted if needed. 55 | // 56 | // [1]: https://github.com/libyal/dtformats/blob/main/documentation/Utmp%20login%20records%20format.asciidoc 57 | let r#type = libc::c_short::from_le_bytes(type_bytes); 58 | 59 | if r#type != libc::USER_PROCESS { 60 | continue; 61 | } 62 | 63 | const USER_OFFSET: usize = std::mem::offset_of!(libc::utmpx, ut_user); 64 | 65 | let user = std::ffi::CStr::from_bytes_until_nul(&buf[USER_OFFSET..]) 66 | .map_err(|error| { 67 | std::io::Error::new(std::io::ErrorKind::InvalidData, error) 68 | }) 69 | .map_err(crate::session::Error::action)?; 70 | 71 | use std::os::unix::ffi::OsStrExt as _; 72 | let user = std::ffi::OsStr::from_bytes(user.to_bytes()); 73 | 74 | // TODO: https://github.com/rust-lang/rust/issues/60896 - Refactor once 75 | //``hash_set_entry` is stabilized. 76 | if !usernames.contains(user) { 77 | usernames.insert(user.to_owned()); 78 | } 79 | } 80 | 81 | for username in usernames { 82 | session.reply(Item { 83 | username, 84 | })?; 85 | } 86 | 87 | Ok(()) 88 | } 89 | 90 | #[cfg(not(target_os = "linux"))] 91 | pub fn handle(_: &mut S, _: ()) -> crate::session::Result<()> 92 | where 93 | S: crate::session::Session, 94 | { 95 | use std::io::{Error, ErrorKind}; 96 | Err(crate::session::Error::action(Error::from(ErrorKind::Unsupported))) 97 | } 98 | 99 | #[cfg(target_os = "linux")] 100 | impl crate::request::Args for Args { 101 | 102 | type Proto = rrg_proto::list_utmp_users::Args; 103 | 104 | fn from_proto(mut proto: Self::Proto) -> Result { 105 | use crate::request::ParseArgsError; 106 | 107 | let path = std::path::PathBuf::try_from(proto.take_path()) 108 | .map_err(|error| ParseArgsError::invalid_field("path", error))?; 109 | 110 | Ok(Args { 111 | path, 112 | }) 113 | } 114 | } 115 | 116 | #[cfg(target_os = "linux")] 117 | impl crate::response::Item for Item { 118 | 119 | type Proto = rrg_proto::list_utmp_users::Result; 120 | 121 | fn into_proto(self) -> Self::Proto { 122 | use std::os::unix::ffi::OsStringExt as _; 123 | 124 | let mut proto = rrg_proto::list_utmp_users::Result::new(); 125 | proto.set_username(self.username.into_vec()); 126 | 127 | proto 128 | } 129 | } 130 | 131 | #[cfg(target_os = "linux")] 132 | #[cfg(test)] 133 | mod tests { 134 | 135 | use super::*; 136 | 137 | #[test] 138 | // Looks like `utmp` entries can have different size depending on the 139 | // platform (e.g. on `aarch64` it has a size of 400 bytes) and so `x86_64` 140 | // samples will not work there. 141 | #[cfg_attr(not(target_arch = "x86_64"), ignore)] 142 | fn handle_custom_utmp_file() { 143 | use std::io::Write as _; 144 | 145 | let mut file = tempfile::NamedTempFile::new() 146 | .unwrap(); 147 | 148 | // Actual non`USERS_PROCESS` entry. 149 | file.write(&[ 150 | &b"\x02\0\0\0\0\0\0\0\x7e\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 151 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x7e\x7e\0\0"[..], 152 | &b"reboot\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 153 | &b"\0\0\0\0\06\x2e10\x2e11\x2d1rfoobar\x2damd64\0\0"[..], 154 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 155 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 156 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 157 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 158 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 159 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 160 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 161 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 162 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 163 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 164 | &b"\x3b\x1f\xc0g\xcf\x20\x0d\0\0\0\0\0\0\0\0\0\0\0\0"[..], 165 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 166 | &b"\0"[..], 167 | ].concat()).unwrap(); 168 | 169 | // Actual `USER_PROCESS` entry. 170 | file.write(&[ 171 | &b"\x07\0\0\0p\x0d\0\0tty2\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 172 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0foobarquux"[..], 173 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0tty2"[..], 174 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 175 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 176 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 177 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 178 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 179 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 180 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 181 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 182 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 183 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 184 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0I\x1f\xc0"[..], 185 | &b"g\x1c\xb5\x0d\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 186 | &b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"[..], 187 | ].concat()).unwrap(); 188 | 189 | file.flush().unwrap(); 190 | 191 | let args = Args { 192 | path: file.path().to_path_buf(), 193 | }; 194 | 195 | let mut session = crate::session::FakeSession::new(); 196 | assert!(handle(&mut session, args).is_ok()); 197 | 198 | assert_eq!(session.reply_count(), 1); 199 | assert_eq!(session.reply::(0).username, "foobarquux"); 200 | } 201 | 202 | #[test] 203 | #[cfg_attr(not(feature = "test-wtmp"), ignore)] 204 | fn handle_var_log_wtmp_no_dupes() { 205 | let args = Args { 206 | path: "/var/log/wtmp".into(), 207 | }; 208 | 209 | let mut session = crate::session::FakeSession::new(); 210 | assert!(handle(&mut session, args).is_ok()); 211 | 212 | let replies = session.replies::() 213 | .collect::>(); 214 | 215 | let mut replies_dedup = replies.clone(); 216 | replies_dedup.sort_by_key(|item| &item.username); 217 | replies_dedup.dedup_by_key(|item| &item.username); 218 | 219 | assert_eq!(replies.len(), replies_dedup.len()); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /crates/rrg/src/action/query_wmi.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Use of this source code is governed by an MIT-style license that can be found 4 | // in the LICENSE file or at https://opensource.org/licenses/MIT. 5 | 6 | /// Arguments of the `query_wmi` action. 7 | #[cfg(target_family = "windows")] 8 | pub struct Args { 9 | /// WMI namespace object path [1] to use for the query. 10 | /// 11 | /// [1]: https://learn.microsoft.com/en-us/windows/win32/wmisdk/describing-a-wmi-namespace-object-path 12 | namespace: std::ffi::OsString, 13 | /// WQL query [1] to run. 14 | /// 15 | /// [1]: https://learn.microsoft.com/en-us/windows/win32/wmisdk/wql-sql-for-wmi 16 | query: std::ffi::OsString, 17 | } 18 | 19 | /// A result of the `query_wmi` action. 20 | #[cfg(target_family = "windows")] 21 | struct Item { 22 | /// Single row of the query result. 23 | row: wmi::QueryRow, 24 | } 25 | 26 | /// Handles invocations of the `query_wmi` action. 27 | #[cfg(target_family = "windows")] 28 | pub fn handle(session: &mut S, args: Args) -> crate::session::Result<()> 29 | where 30 | S: crate::session::Session, 31 | { 32 | let namespace = wmi::namespace(&args.namespace) 33 | .map_err(crate::session::Error::action)?; 34 | 35 | let query = namespace.query(&args.query); 36 | 37 | let rows = query.rows() 38 | .map_err(crate::session::Error::action)?; 39 | 40 | // Depending on the implementation, sometimes even if the query is invalid, 41 | // we get an error from the system only after we poll for the first row (but 42 | // sometimes we get it immediately, e.g. when running under Wine). 43 | // 44 | // We also do not want to fail the entire action if only some of the rows 45 | // failed to be fetched. Thus, we want to model the behaviour where we fail 46 | // the entire action if there are no rows at all but we do not do it if 47 | // there are any rows to be returned. 48 | // 49 | // We introduce auxiliary `Status` struct encoding a state machine that 50 | // implements such logic. 51 | enum Status { 52 | /// There were no rows at all. 53 | None, 54 | /// There were some rows returned. 55 | Some, 56 | /// There were only errors reported. 57 | Error(std::io::Error), 58 | } 59 | 60 | let mut status: Status = Status::None; 61 | 62 | for row in rows { 63 | let row = match row { 64 | Ok(row) => row, 65 | Err(error) => { 66 | log::error!("failed to obtain WMI query row: {}", error); 67 | // If there were no rows at all so far we keep the error in case 68 | // there are not going to be any further. We only keep the first 69 | // error—this is a rather arbitrary choice as all errors will be 70 | // logged anyway but might offer better experience when handling 71 | // these on the GRR flow level. 72 | if matches!(status, Status::None) { 73 | status = Status::Error(error); 74 | } 75 | continue; 76 | } 77 | }; 78 | 79 | session.reply(Item { row })?; 80 | // We reported a row, so we unconditionally change the status. Even if 81 | // there was an error, we will not fail the action at this point. 82 | status = Status::Some; 83 | } 84 | 85 | // As mentioned, we succeed the action if there were any rows reported or 86 | // if there was nothing at all. However, if there were only errors, we fail 87 | // it with the first error that occurred. 88 | match status { 89 | Status::None | Status::Some => Ok(()), 90 | Status::Error(error) => Err(crate::session::Error::action(error)), 91 | } 92 | } 93 | 94 | /// Handles invocations of the `query_wmi` action. 95 | #[cfg(target_family = "unix")] 96 | pub fn handle(_: &mut S, _: ()) -> crate::session::Result<()> 97 | where 98 | S: crate::session::Session, 99 | { 100 | use std::io::{Error, ErrorKind}; 101 | Err(crate::session::Error::action(Error::from(ErrorKind::Unsupported))) 102 | } 103 | 104 | #[cfg(target_family = "windows")] 105 | impl crate::request::Args for Args { 106 | 107 | type Proto = rrg_proto::query_wmi::Args; 108 | 109 | fn from_proto(mut proto: Self::Proto) -> Result { 110 | // TODO(@panhania): For the time being we use the default namespace in 111 | // case it is not provided, but eventually we should require GRR server 112 | // to always set it explicitly. 113 | let namespace = if proto.namespace().is_empty() { 114 | std::ffi::OsString::from("root\\cimv2") 115 | } else { 116 | std::ffi::OsString::from(proto.take_namespace()) 117 | }; 118 | 119 | Ok(Args { 120 | namespace, 121 | query: std::ffi::OsString::from(proto.take_query()), 122 | }) 123 | } 124 | } 125 | 126 | #[cfg(target_family = "windows")] 127 | impl crate::response::Item for Item { 128 | 129 | type Proto = rrg_proto::query_wmi::Result; 130 | 131 | fn into_proto(self) -> Self::Proto { 132 | let mut proto = rrg_proto::query_wmi::Result::new(); 133 | 134 | for (name, value) in self.row { 135 | let proto_name = name.to_string_lossy().into_owned(); 136 | let mut proto_value = rrg_proto::query_wmi::Value::new(); 137 | 138 | match value { 139 | wmi::QueryValue::None => (), 140 | wmi::QueryValue::Bool(bool) => { 141 | proto_value.set_bool(bool) 142 | } 143 | wmi::QueryValue::U8(u8) => { 144 | proto_value.set_uint(u64::from(u8)) 145 | } 146 | wmi::QueryValue::I8(i8) => { 147 | proto_value.set_int(i64::from(i8)) 148 | } 149 | wmi::QueryValue::U16(u16) => { 150 | proto_value.set_uint(u64::from(u16)) 151 | } 152 | wmi::QueryValue::I16(i16) => { 153 | proto_value.set_int(i64::from(i16)) 154 | } 155 | wmi::QueryValue::U32(u32) => { 156 | proto_value.set_uint(u64::from(u32)) 157 | } 158 | wmi::QueryValue::I32(i32) => { 159 | proto_value.set_int(i64::from(i32)) 160 | } 161 | wmi::QueryValue::U64(u64) => { 162 | proto_value.set_uint(u64) 163 | } 164 | wmi::QueryValue::I64(i64) => { 165 | proto_value.set_int(i64) 166 | } 167 | wmi::QueryValue::F32(f32) => { 168 | proto_value.set_float(f32) 169 | } 170 | wmi::QueryValue::F64(f64) => { 171 | proto_value.set_double(f64) 172 | } 173 | wmi::QueryValue::String(string) => { 174 | proto_value.set_string(string.to_string_lossy().into_owned()) 175 | } 176 | wmi::QueryValue::Unsupported(_) => (), 177 | } 178 | 179 | proto.mut_row().insert(proto_name, proto_value); 180 | } 181 | 182 | proto 183 | } 184 | } 185 | 186 | #[cfg(test)] 187 | #[cfg(target_family = "windows")] 188 | mod tests { 189 | 190 | use super::*; 191 | 192 | #[test] 193 | fn handle_invalid_query() { 194 | let args = Args { 195 | namespace: "root\\cimv2".into(), 196 | query: " 197 | INSERT 198 | INTO 199 | Win32_OperatingSystem (Name, Version) 200 | VALUES 201 | ('Foo', '1.3.3.7') 202 | ".into(), 203 | }; 204 | 205 | let mut session = crate::session::FakeSession::new(); 206 | assert!(handle(&mut session, args).is_err()); 207 | 208 | } 209 | 210 | #[test] 211 | fn handle_no_rows() { 212 | let args = Args { 213 | namespace: "root\\cimv2".into(), 214 | query: " 215 | SELECT 216 | * 217 | FROM 218 | Win32_OperatingSystem 219 | WHERE 220 | FreePhysicalMemory < 0 221 | ".into(), 222 | }; 223 | 224 | let mut session = crate::session::FakeSession::new(); 225 | assert!(handle(&mut session, args).is_ok()); 226 | assert_eq!(session.reply_count(), 0); 227 | } 228 | 229 | #[test] 230 | fn handle_some_rows() { 231 | let args = Args { 232 | namespace: "root\\cimv2".into(), 233 | query: " 234 | SELECT 235 | * 236 | FROM 237 | Win32_OperatingSystem 238 | WHERE 239 | FreePhysicalMemory >= 0 240 | ".into(), 241 | }; 242 | 243 | let mut session = crate::session::FakeSession::new(); 244 | assert!(handle(&mut session, args).is_ok()); 245 | assert_eq!(session.reply_count(), 1); 246 | } 247 | } 248 | --------------------------------------------------------------------------------