From bc67e7d9ab5a59323f7ebf55a5e549ad846141fb Mon Sep 17 00:00:00 2001 From: Zach Wasserman Date: Tue, 12 Aug 2025 15:50:53 -0700 Subject: [PATCH] Add `system_profiler` table for macOS (#8645) For https://github.com/fleetdm/fleet/issues/30119 --- osquery/tables/system/CMakeLists.txt | 1 + .../tables/system/darwin/system_profiler.mm | 129 ++++++++++++++++++ specs/CMakeLists.txt | 1 + specs/darwin/system_profiler.table | 11 ++ tests/integration/tables/CMakeLists.txt | 1 + tests/integration/tables/system_profiler.cpp | 56 ++++++++ 6 files changed, 199 insertions(+) create mode 100644 osquery/tables/system/darwin/system_profiler.mm create mode 100644 specs/darwin/system_profiler.table create mode 100644 tests/integration/tables/system_profiler.cpp diff --git a/osquery/tables/system/CMakeLists.txt b/osquery/tables/system/CMakeLists.txt index b1484330619..087796ed322 100644 --- a/osquery/tables/system/CMakeLists.txt +++ b/osquery/tables/system/CMakeLists.txt @@ -138,6 +138,7 @@ function(generateOsqueryTablesSystemSystemtable) darwin/managed_policies.cpp darwin/mdfind.mm darwin/mdls.mm + darwin/system_profiler.mm darwin/mounts.cpp darwin/nfs_shares.cpp darwin/nvram.cpp diff --git a/osquery/tables/system/darwin/system_profiler.mm b/osquery/tables/system/darwin/system_profiler.mm new file mode 100644 index 00000000000..32919feeaa0 --- /dev/null +++ b/osquery/tables/system/darwin/system_profiler.mm @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2014-present, The osquery authors + * + * This source code is licensed as defined by the LICENSE file found in the + * root directory of this source tree. + * + * SPDX-License-Identifier: (Apache-2.0 OR GPL-2.0-only) + */ + +#include +#include +#include +#include +#include +#include + +#include + +namespace osquery { +namespace tables { + +id convertForJSON(id obj) { + @try { + if ([obj isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary* dict = [NSMutableDictionary + dictionaryWithCapacity:[(NSDictionary*)obj count]]; + for (id key in (NSDictionary*)obj) { + dict[key] = convertForJSON([(NSDictionary*)obj objectForKey:key]); + } + return dict; + } else if ([obj isKindOfClass:[NSArray class]]) { + NSMutableArray* arr = + [NSMutableArray arrayWithCapacity:[(NSArray*)obj count]]; + for (id item in (NSArray*)obj) { + [arr addObject:convertForJSON(item)]; + } + return arr; + } else if ([obj isKindOfClass:[NSDate class]]) { + // formatter will be autoreleased when the pool is drained + NSDateFormatter* formatter = [[NSDateFormatter alloc] init]; + [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZ"]; + NSString* result = [formatter stringFromDate:(NSDate*)obj]; + return result; + } else if ([obj isKindOfClass:[NSData class]]) { + return [(NSData*)obj base64EncodedStringWithOptions:0]; + } else if ([obj isKindOfClass:[NSURL class]]) { + return [(NSURL*)obj absoluteString]; + } else if ([obj isKindOfClass:[NSUUID class]]) { + return [(NSUUID*)obj UUIDString]; + } else if ([obj isKindOfClass:[NSNumber class]] || + [obj isKindOfClass:[NSString class]]) { + // These types are already JSON-safe + return obj; + } else { + // For any other type, try to get a string representation + return [obj description]; + } + } @catch (NSException* exception) { + LOG(WARNING) << "Exception in convertForJSON: " + << [[exception reason] UTF8String]; + return @"[Error converting object]"; + } +} + +std::string objectToJson(id data) { + @try { + NSError* error = nil; + id safeDict = convertForJSON(data); + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:safeDict + options:0 + error:&error]; + if (error != nil || jsonData == nil) { + LOG(WARNING) << "JSON serialization failed: " + << (error ? [[error localizedDescription] UTF8String] + : "unknown error"); + return ""; + } + NSString* jsonString = [[NSString alloc] initWithData:jsonData + encoding:NSUTF8StringEncoding]; + if (jsonString == nil) { + LOG(WARNING) << "Failed to create string from JSON data"; + return ""; + } + return stringFromCFString((__bridge CFStringRef)jsonString); + } @catch (NSException* exception) { + LOG(WARNING) << "Exception in nsDictionaryToJson: " + << [[exception reason] UTF8String]; + return ""; + } +} + +QueryData genSystemProfilerResults(QueryContext& context) { + QueryData results; + auto data_type_constraints = context.constraints["data_type"].getAll(EQUALS); + + @autoreleasepool { + for (const auto& dataType : data_type_constraints) { + NSDictionary* __autoreleasing report = nullptr; + auto status = getSystemProfilerReport(dataType, report); + + if (!status.ok()) { + LOG(WARNING) << "Failed to get system profiler report for " << dataType + << ": " << status.getMessage(); + continue; + } + + if (report == nullptr) { + LOG(WARNING) << "System profiler report is null for " << dataType; + continue; + } + + id items = [report objectForKey:@"_items"]; + if (items == nil) { + LOG(WARNING) << "System profiler report items is null for " << dataType; + continue; + } + + Row r; + r["data_type"] = dataType; + r["value"] = objectToJson(items); + results.push_back(r); + } + } + + return results; +} + +} // namespace tables +} // namespace osquery \ No newline at end of file diff --git a/specs/CMakeLists.txt b/specs/CMakeLists.txt index 1f33b12bf07..22fe2df11a1 100644 --- a/specs/CMakeLists.txt +++ b/specs/CMakeLists.txt @@ -144,6 +144,7 @@ function(generateNativeTables) "darwin/sip_config.table:macos" "darwin/smc_keys.table:macos" "darwin/system_extensions.table:macos" + "darwin/system_profiler.table:macos" "darwin/temperature_sensors.table:macos" "darwin/time_machine_backups.table:macos" "darwin/time_machine_destinations.table:macos" diff --git a/specs/darwin/system_profiler.table b/specs/darwin/system_profiler.table new file mode 100644 index 00000000000..51edaa813aa --- /dev/null +++ b/specs/darwin/system_profiler.table @@ -0,0 +1,11 @@ +table_name("system_profiler") +description("Query system_profiler data types and return the full result as JSON. Returns only the data types specified in the constraints. See available data types with `system_profiler -listDataTypes`.") +schema([ + Column("data_type", TEXT, "The system profiler data type (e.g., SPHardwareDataType)", index=True, required=True), + Column("value", TEXT, "A JSON representation of the full result dictionary for the data type"), +]) +implementation("system_profiler@genSystemProfilerResults") +examples([ + "select * from system_profiler where data_type = 'SPHardwareDataType';", + "select json_extract(value, '$.platform_UUID') from system_profiler where data_type = 'SPHardwareDataType';" +]) \ No newline at end of file diff --git a/tests/integration/tables/CMakeLists.txt b/tests/integration/tables/CMakeLists.txt index bf48b47980a..55f1108617d 100644 --- a/tests/integration/tables/CMakeLists.txt +++ b/tests/integration/tables/CMakeLists.txt @@ -245,6 +245,7 @@ function(generateTestsIntegrationTablesTestsTest) managed_policies.cpp mdfind.cpp nfs_shares.cpp + system_profiler.cpp nvram.cpp package_bom.cpp package_install_history.cpp diff --git a/tests/integration/tables/system_profiler.cpp b/tests/integration/tables/system_profiler.cpp new file mode 100644 index 00000000000..ff1a8338cc7 --- /dev/null +++ b/tests/integration/tables/system_profiler.cpp @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2014-present, The osquery authors + * + * This source code is licensed as defined by the LICENSE file found in the + * root directory of this source tree. + * + * SPDX-License-Identifier: (Apache-2.0 OR GPL-2.0-only) + */ + +#include + +namespace osquery { +namespace table_tests { + +class SystemProfilerTest : public testing::Test { + protected: + void SetUp() override { + setUpEnvironment(); + } +}; + +TEST_F(SystemProfilerTest, test_system_profiler_no_constraints) { + auto const data = execute_query("select * from system_profiler limit 10"); + ASSERT_EQ(data.size(), 0UL); +} + +TEST_F(SystemProfilerTest, test_system_profiler_hardware_data) { + auto const data = execute_query( + "select * from system_profiler where data_type = 'SPHardwareDataType' " + "limit 5"); + ASSERT_EQ(data.size(), 1UL); + + for (const auto& row : data) { + EXPECT_EQ(row.at("data_type"), "SPHardwareDataType"); + EXPECT_FALSE(row.at("value").empty()); + } +} + +TEST_F(SystemProfilerTest, test_system_profiler_in_clause) { + auto const data = execute_query( + "select * from system_profiler where data_type IN ('SPEthernetDataType', " + "'SPFirewallDataType', 'SPMemoryDataType') limit 10"); + ASSERT_EQ(data.size(), 3UL); + + std::set expected_types = { + "SPEthernetDataType", "SPFirewallDataType", "SPMemoryDataType"}; + + for (const auto& row : data) { + EXPECT_TRUE(expected_types.find(row.at("data_type")) != + expected_types.end()); + EXPECT_FALSE(row.at("value").empty()); + } +} + +} // namespace table_tests +} // namespace osquery \ No newline at end of file