mirror of
https://github.com/valitydev/osquery-1.git
synced 2024-11-06 09:35:20 +00:00
Support for "matches" and "js" keys from "content_scripts" in the chrome_extensions table (#6140)
Co-authored-by: William Woodruff <william@yossarian.net>
This commit is contained in:
parent
57ffaa89fc
commit
3de799ef07
@ -6,6 +6,8 @@
|
||||
* the LICENSE file found in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#include <osquery/logger.h>
|
||||
|
||||
#include <osquery/tables/applications/browser_utils.h>
|
||||
#include <osquery/utils/info/platform_type.h>
|
||||
|
||||
@ -18,39 +20,49 @@ namespace tables {
|
||||
#pragma warning(disable : 4503)
|
||||
#endif
|
||||
|
||||
QueryData genChromeExtensions(QueryContext& context) {
|
||||
static std::vector<fs::path> getChromePaths() {
|
||||
std::vector<fs::path> chromePaths;
|
||||
|
||||
/// Each home directory will include custom extensions.
|
||||
fs::path chrome_path;
|
||||
if (isPlatform(PlatformType::TYPE_WINDOWS)) {
|
||||
chrome_path =
|
||||
"\\AppData\\Local\\Google\\Chrome\\User Data\\%\\Extensions\\";
|
||||
chromePaths.push_back(
|
||||
"\\AppData\\Local\\Google\\Chrome\\User Data\\%\\Extensions\\");
|
||||
} else if (isPlatform(PlatformType::TYPE_OSX)) {
|
||||
chrome_path = "/Library/Application Support/Google/Chrome/%/Extensions/";
|
||||
chromePaths.push_back(
|
||||
"/Library/Application Support/Google/Chrome/%/Extensions/");
|
||||
} else {
|
||||
chrome_path = "/.config/google-chrome/%/Extensions/";
|
||||
}
|
||||
fs::path brave_path;
|
||||
if (isPlatform(PlatformType::TYPE_WINDOWS)) {
|
||||
brave_path = "\\AppData\\Roaming\\brave\\Extensions\\";
|
||||
} else if (isPlatform(PlatformType::TYPE_OSX)) {
|
||||
brave_path =
|
||||
"/Library/Application "
|
||||
"Support/BraveSoftware/Brave-Browser/%/Extensions/";
|
||||
} else {
|
||||
brave_path = "/.config/BraveSoftware/Brave-Browser/%/Extensions/";
|
||||
}
|
||||
fs::path chromium_path;
|
||||
if (isPlatform(PlatformType::TYPE_WINDOWS)) {
|
||||
chromium_path = "\\AppData\\Local\\Chromium\\Extensions\\";
|
||||
} else if (isPlatform(PlatformType::TYPE_OSX)) {
|
||||
chromium_path = "/Library/Application Support/Chromium/%/Extensions/";
|
||||
} else {
|
||||
chromium_path = "/.config/chromium/%/Extensions/";
|
||||
chromePaths.push_back("/.config/google-chrome/%/Extensions/");
|
||||
}
|
||||
|
||||
return genChromeBasedExtensions(context,
|
||||
{chrome_path, brave_path, chromium_path});
|
||||
if (isPlatform(PlatformType::TYPE_WINDOWS)) {
|
||||
chromePaths.push_back("\\AppData\\Roaming\\brave\\Extensions\\");
|
||||
} else if (isPlatform(PlatformType::TYPE_OSX)) {
|
||||
chromePaths.push_back(
|
||||
"/Library/Application "
|
||||
"Support/BraveSoftware/Brave-Browser/%/Extensions/");
|
||||
} else {
|
||||
chromePaths.push_back("/.config/BraveSoftware/Brave-Browser/%/Extensions/");
|
||||
}
|
||||
|
||||
if (isPlatform(PlatformType::TYPE_WINDOWS)) {
|
||||
chromePaths.push_back("\\AppData\\Local\\Chromium\\Extensions\\");
|
||||
} else if (isPlatform(PlatformType::TYPE_OSX)) {
|
||||
chromePaths.push_back(
|
||||
"/Library/Application Support/Chromium/%/Extensions/");
|
||||
} else {
|
||||
chromePaths.push_back("/.config/chromium/%/Extensions/");
|
||||
}
|
||||
|
||||
return chromePaths;
|
||||
}
|
||||
|
||||
QueryData genChromeExtensions(QueryContext& context) {
|
||||
return genChromeBasedExtensions(context, getChromePaths());
|
||||
}
|
||||
|
||||
QueryData genChromeExtensionContentScripts(QueryContext& context) {
|
||||
return genChromeBasedExtensionContentScripts(context, getChromePaths());
|
||||
}
|
||||
|
||||
} // namespace tables
|
||||
} // namespace osquery
|
||||
|
@ -20,6 +20,17 @@ namespace osquery {
|
||||
namespace tables {
|
||||
namespace {
|
||||
|
||||
using ChromeExtensionContentScriptMap =
|
||||
std::map<std::tuple<std::string, std::string>,
|
||||
std::set<std::tuple<std::string, std::string>>>;
|
||||
|
||||
using ChromeContentScriptDetails =
|
||||
std::vector<std::map<std::string, std::vector<std::string>>>;
|
||||
|
||||
using ChromeUserExtensions =
|
||||
std::tuple<std::string /* uid */,
|
||||
std::vector<std::string> /* extension_paths*/>;
|
||||
|
||||
#define kManifestFile "/manifest.json"
|
||||
|
||||
const std::map<std::string, std::string> kExtensionKeys = {
|
||||
@ -35,6 +46,46 @@ const std::string kExtensionPermissionKey = "permissions";
|
||||
const std::string kExtensionOptionalPermissionKey = "optional_permissions";
|
||||
const std::string kProfilePreferencesFile = "Preferences";
|
||||
const std::string kProfilePreferenceKey = "profile";
|
||||
const std::string kScriptKey = "js";
|
||||
const std::string kMatchesKey = "matches";
|
||||
|
||||
std::vector<ChromeUserExtensions> chromeExtensionPathsByUser(
|
||||
const QueryData& users, const std::vector<fs::path>& chromePaths) {
|
||||
std::vector<ChromeUserExtensions> extensionPathsByUser;
|
||||
|
||||
for (const auto& row : users) {
|
||||
if (row.count("uid") > 0 && row.count("directory") > 0) {
|
||||
// For each user, enumerate all of their chrome profiles.
|
||||
std::vector<std::string> profiles;
|
||||
for (const auto& chromePath : chromePaths) {
|
||||
fs::path extension_path = row.at("directory") / chromePath;
|
||||
if (!resolveFilePattern(extension_path, profiles, GLOB_FOLDERS).ok()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For each profile list each extension in the Extensions directory.
|
||||
for (const auto& profile : profiles) {
|
||||
std::vector<std::string> unversionedExtensions = {};
|
||||
listDirectoriesInDirectory(profile, unversionedExtensions);
|
||||
|
||||
if (unversionedExtensions.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<std::string> extensionPaths;
|
||||
for (const auto& unversionedExtension : unversionedExtensions) {
|
||||
listDirectoriesInDirectory(unversionedExtension, extensionPaths);
|
||||
}
|
||||
|
||||
extensionPathsByUser.push_back(
|
||||
std::make_tuple(row.at("uid"), extensionPaths));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extensionPathsByUser;
|
||||
}
|
||||
|
||||
Status getChromeProfileName(std::string& name, const fs::path& path) {
|
||||
name.clear();
|
||||
@ -97,6 +148,41 @@ const std::string genPermissions(const std::string& permissionTypeKey,
|
||||
return permission_list;
|
||||
}
|
||||
|
||||
ChromeContentScriptDetails genContentScriptDetail(const pt::ptree& tree) {
|
||||
ChromeContentScriptDetails details;
|
||||
|
||||
if (const auto& content_script_array =
|
||||
tree.get_child_optional("content_scripts")) {
|
||||
for (const auto& content_script : content_script_array.get()) {
|
||||
std::map<std::string, std::vector<std::string>> detail;
|
||||
|
||||
if (const auto& js_script_array =
|
||||
content_script.second.get_child_optional(kScriptKey)) {
|
||||
for (const auto& js_script : js_script_array.get()) {
|
||||
if (const auto& js_script_value =
|
||||
js_script.second.get_value_optional<std::string>()) {
|
||||
detail[kScriptKey].push_back(js_script_value.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto& match_array =
|
||||
content_script.second.get_child_optional(kMatchesKey)) {
|
||||
for (const auto& match : match_array.get()) {
|
||||
if (const auto& match_value =
|
||||
match.second.get_value_optional<std::string>()) {
|
||||
detail[kMatchesKey].push_back(match_value.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
details.push_back(detail);
|
||||
}
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
void genExtension(const std::string& uid,
|
||||
const std::string& path,
|
||||
const std::string& profile_name,
|
||||
@ -174,58 +260,116 @@ void genExtension(const std::string& uid,
|
||||
|
||||
r["identifier"] = fs::path(path).parent_path().parent_path().leaf().string();
|
||||
r["path"] = path;
|
||||
|
||||
results.push_back(r);
|
||||
}
|
||||
|
||||
void genExtensionContentScripts(
|
||||
const std::string& path,
|
||||
ChromeExtensionContentScriptMap& contentScriptMap) {
|
||||
std::string json_data;
|
||||
if (!forensicReadFile(path + kManifestFile, json_data).ok()) {
|
||||
VLOG(1) << "Could not read file: " << path + kManifestFile;
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the extension metadata into a JSON blob, then property tree.
|
||||
pt::ptree tree;
|
||||
try {
|
||||
std::stringstream json_stream;
|
||||
json_stream << json_data;
|
||||
pt::read_json(json_stream, tree);
|
||||
} catch (const pt::json_parser::json_parser_error& /* e */) {
|
||||
VLOG(1) << "Could not parse JSON from: " << path + kManifestFile;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string& version = tree.get<std::string>("version", "");
|
||||
auto& scriptMatchPairs = contentScriptMap[std::make_tuple(
|
||||
fs::path(path).parent_path().parent_path().leaf().string(), version)];
|
||||
|
||||
auto contentScriptDetail = genContentScriptDetail(tree);
|
||||
for (auto& contentScript : contentScriptDetail) {
|
||||
for (auto& script : contentScript[kScriptKey]) {
|
||||
if (contentScript[kMatchesKey].empty()) {
|
||||
scriptMatchPairs.insert(std::make_tuple(script, ""));
|
||||
} else {
|
||||
for (auto& match : contentScript[kMatchesKey]) {
|
||||
scriptMatchPairs.insert(std::make_tuple(script, match));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QueryData genChromeBasedExtensions(QueryContext& context,
|
||||
const std::vector<fs::path>& chromePaths) {
|
||||
QueryData results;
|
||||
|
||||
auto users = usersFromContext(context);
|
||||
for (const auto& row : users) {
|
||||
if (row.count("uid") > 0 && row.count("directory") > 0) {
|
||||
// For each user, enumerate all of their chrome profiles.
|
||||
std::vector<std::string> profiles;
|
||||
for (const auto& chromePath : chromePaths) {
|
||||
fs::path extension_path = row.at("directory") / chromePath;
|
||||
if (!resolveFilePattern(extension_path, profiles, GLOB_FOLDERS).ok()) {
|
||||
continue;
|
||||
}
|
||||
const auto& extensionPathsByUser =
|
||||
chromeExtensionPathsByUser(usersFromContext(context), chromePaths);
|
||||
|
||||
// For each profile list each extension in the Extensions directory.
|
||||
for (const auto& profile : profiles) {
|
||||
std::vector<std::string> extensions = {};
|
||||
listDirectoriesInDirectory(profile, extensions);
|
||||
for (const auto& userExtensionPaths : extensionPathsByUser) {
|
||||
const auto& uid = std::get<0>(userExtensionPaths);
|
||||
std::map<fs::path, std::string> profileNameMap;
|
||||
|
||||
if (extensions.empty()) {
|
||||
continue;
|
||||
}
|
||||
for (const auto& version : std::get<1>(userExtensionPaths)) {
|
||||
const auto& profile_path = fs::path(version)
|
||||
.parent_path()
|
||||
.parent_path()
|
||||
.parent_path()
|
||||
.parent_path();
|
||||
|
||||
auto profile_path = fs::path(profile).parent_path().parent_path();
|
||||
|
||||
std::string profile_name;
|
||||
auto status = getChromeProfileName(profile_name, profile_path);
|
||||
if (!status.ok()) {
|
||||
LOG(WARNING) << "Getting Chrome profile name failed: "
|
||||
<< status.getMessage();
|
||||
}
|
||||
|
||||
// Generate an addons list from their extensions JSON.
|
||||
std::vector<std::string> versions;
|
||||
for (const auto& extension : extensions) {
|
||||
listDirectoriesInDirectory(extension, versions);
|
||||
}
|
||||
|
||||
// Extensions use /<EXTENSION>/<VERSION>/manifest.json.
|
||||
for (const auto& version : versions) {
|
||||
genExtension(row.at("uid"), version, profile_name, results);
|
||||
}
|
||||
auto it = profileNameMap.find(profile_path);
|
||||
if (it == profileNameMap.end()) {
|
||||
auto status =
|
||||
getChromeProfileName(profileNameMap[profile_path], profile_path);
|
||||
if (!status.ok()) {
|
||||
LOG(WARNING) << "Getting Chrome profile name failed: "
|
||||
<< status.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
genExtension(uid, version, profileNameMap[profile_path], results);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
QueryData genChromeBasedExtensionContentScripts(
|
||||
QueryContext& context, const std::vector<fs::path>& chromePaths) {
|
||||
QueryData results;
|
||||
|
||||
// Extensions are frequently duplicated across profiles and
|
||||
// Chrome installations, so we construct a map of
|
||||
// (extension_id, version) -> {(script, match)}
|
||||
// for deduplication purposes.
|
||||
ChromeExtensionContentScriptMap contentScriptMap;
|
||||
|
||||
const auto& extensionPathsByUser =
|
||||
chromeExtensionPathsByUser(usersFromContext(context), chromePaths);
|
||||
for (const auto& userExtensionPaths : extensionPathsByUser) {
|
||||
for (const auto& version : std::get<1>(userExtensionPaths)) {
|
||||
genExtensionContentScripts(version, contentScriptMap);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& it : contentScriptMap) {
|
||||
Row r;
|
||||
|
||||
r["identifier"] = std::get<0>(it.first);
|
||||
r["version"] = std::get<1>(it.first);
|
||||
|
||||
for (const auto& scriptMatchPair : it.second) {
|
||||
r["script"] = std::get<0>(scriptMatchPair);
|
||||
r["match"] = std::get<1>(scriptMatchPair);
|
||||
results.push_back(r);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace tables
|
||||
} // namespace osquery
|
||||
|
@ -28,6 +28,9 @@ namespace tables {
|
||||
QueryData genChromeBasedExtensions(QueryContext& context,
|
||||
const std::vector<fs::path>& chrome_paths);
|
||||
|
||||
QueryData genChromeBasedExtensionContentScripts(
|
||||
QueryContext& context, const std::vector<fs::path>& chrome_paths);
|
||||
|
||||
/// A helper check to rename bool-type values as 1 or 0.
|
||||
inline void jsonBoolAsInt(std::string& s) {
|
||||
auto expected = tryTo<bool>(s);
|
||||
@ -35,5 +38,5 @@ inline void jsonBoolAsInt(std::string& s) {
|
||||
s = expected.get() ? "1" : "0";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace tables
|
||||
} // namespace osquery
|
||||
|
@ -819,6 +819,7 @@ osquery_gentable_cxx_library(
|
||||
"carbon_black_info.table",
|
||||
"carves.table",
|
||||
"chrome_extensions.table",
|
||||
"chrome_extension_content_scripts.table",
|
||||
"cpuid.table",
|
||||
"curl.table",
|
||||
"curl_certificate.table",
|
||||
|
@ -33,6 +33,7 @@ function(generateNativeTables)
|
||||
carbon_black_info.table
|
||||
carves.table
|
||||
chrome_extensions.table
|
||||
chrome_extension_content_scripts.table
|
||||
cpuid.table
|
||||
curl.table
|
||||
curl_certificate.table
|
||||
|
20
specs/chrome_extension_content_scripts.table
Normal file
20
specs/chrome_extension_content_scripts.table
Normal file
@ -0,0 +1,20 @@
|
||||
table_name("chrome_extension_content_scripts")
|
||||
description("Chrome browser extension content scripts.")
|
||||
schema([
|
||||
Column("uid", BIGINT, "The local user that owns the extension",
|
||||
index=True),
|
||||
Column("identifier", TEXT, "Extension identifier"),
|
||||
Column("version", TEXT, "Extension-supplied version"),
|
||||
Column("script", TEXT, "The content script used by the extension"),
|
||||
Column("match", TEXT, "The pattern that the script is matched against"),
|
||||
ForeignKey(column="uid", table="users"),
|
||||
])
|
||||
attributes(user_data=True)
|
||||
implementation("applications/browser_chrome@genChromeExtensionContentScripts")
|
||||
examples([
|
||||
"select * from chrome_extensions join chrome_extension_content_scripts using (identifier)",
|
||||
])
|
||||
fuzz_paths([
|
||||
"/Library/Application Support/Google/Chrome/",
|
||||
"/Users",
|
||||
])
|
@ -35,7 +35,9 @@ TEST_F(chromeExtensions, test_sanity) {
|
||||
{"persistent", IntType},
|
||||
{"path", NonEmptyString},
|
||||
{"permissions", NormalType},
|
||||
{"profile", NonEmptyString}};
|
||||
{"profile", NormalType},
|
||||
{"script", NormalType},
|
||||
{"match", NormalType}};
|
||||
validate_rows(data, row_map);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user