]> go.fuhry.dev Git - osquery.git/commitdiff
Initial commit
authorDan Fuhry <dan@fuhry.com>
Wed, 2 Jul 2025 22:03:17 +0000 (18:03 -0400)
committerDan Fuhry <dan@fuhry.com>
Wed, 2 Jul 2025 22:03:17 +0000 (18:03 -0400)
23 files changed:
.bazelversion [new file with mode: 0644]
.gitignore [new file with mode: 0644]
BUILD.bazel [new file with mode: 0644]
LICENSE [new file with mode: 0644]
MODULE.bazel [new file with mode: 0644]
MODULE.bazel.lock [new file with mode: 0644]
README.md [new file with mode: 0644]
cmd/flatpak/BUILD.bazel [new file with mode: 0644]
cmd/flatpak/main.go [new file with mode: 0644]
cmd/pacman/BUILD.bazel [new file with mode: 0644]
cmd/pacman/main.go [new file with mode: 0644]
extcommon/BUILD.bazel [new file with mode: 0644]
extcommon/extcommon.go [new file with mode: 0644]
extcommon/util.go [new file with mode: 0644]
flatpak/BUILD.bazel [new file with mode: 0644]
flatpak/data.go [new file with mode: 0644]
flatpak/plugin.go [new file with mode: 0644]
flatpak/registry.go [new file with mode: 0644]
go.mod [new file with mode: 0644]
go.sum [new file with mode: 0644]
pacman/BUILD.bazel [new file with mode: 0644]
pacman/pacman.go [new file with mode: 0644]
pacman/typed_columns.go [new file with mode: 0644]

diff --git a/.bazelversion b/.bazelversion
new file mode 100644 (file)
index 0000000..2b0aa21
--- /dev/null
@@ -0,0 +1 @@
+8.2.1
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..b5b830b
--- /dev/null
@@ -0,0 +1,4 @@
+bazel-bin
+bazel-osquery
+bazel-out
+bazel-testlogs
diff --git a/BUILD.bazel b/BUILD.bazel
new file mode 100644 (file)
index 0000000..abccfc7
--- /dev/null
@@ -0,0 +1,3 @@
+load("@gazelle//:def.bzl", "gazelle")
+
+gazelle(name = "gazelle")
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..87b3f0e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+Copyright 2025 Dan Fuhry <dan@fuhry.com>
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright 
+   notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in
+   the documentation and/or other materials provided with the
+   distribution.
+
+3. Neither the name of the copyright holder nor the names of its 
+   contributors may be used to endorse or promote products derived
+   from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
+“AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/MODULE.bazel b/MODULE.bazel
new file mode 100644 (file)
index 0000000..2420ab6
--- /dev/null
@@ -0,0 +1,25 @@
+bazel_dep(
+       name = "rules_go",
+       version = "0.55.1",
+)
+
+bazel_dep(
+       name = "gazelle",
+       version = "0.44.0",
+)
+
+go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
+go_sdk.from_file(go_mod = "//:go.mod")
+
+go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
+go_deps.from_file(go_mod = "//:go.mod")
+
+use_repo(
+       go_deps,
+       "com_github_gobwas_glob",
+       "com_github_osquery_osquery_go",
+       "com_github_chrisportman_go_gvariant",
+       "com_github_hashicorp_golang_lru_v2",
+       "com_github_linuxdeepin_go_lib",
+       "com_github_jguer_go_alpm_v2",
+)
\ No newline at end of file
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
new file mode 100644 (file)
index 0000000..7618ad7
--- /dev/null
@@ -0,0 +1,240 @@
+{
+  "lockFileVersion": 18,
+  "registryFileHashes": {
+    "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497",
+    "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2",
+    "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589",
+    "https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0",
+    "https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb",
+    "https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16",
+    "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915",
+    "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
+    "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da",
+    "https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b",
+    "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
+    "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8",
+    "https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d",
+    "https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d",
+    "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a",
+    "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58",
+    "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
+    "https://bcr.bazel.build/modules/bazel_features/1.21.0/source.json": "3e8379efaaef53ce35b7b8ba419df829315a880cb0a030e5bb45c96d6d5ecb5f",
+    "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
+    "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/source.json": "f121b43eeefc7c29efbd51b83d08631e2347297c95aac9764a701f2a6a2bb953",
+    "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84",
+    "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8",
+    "https://bcr.bazel.build/modules/gazelle/0.32.0/MODULE.bazel": "b499f58a5d0d3537f3cf5b76d8ada18242f64ec474d8391247438bf04f58c7b8",
+    "https://bcr.bazel.build/modules/gazelle/0.33.0/MODULE.bazel": "a13a0f279b462b784fb8dd52a4074526c4a2afe70e114c7d09066097a46b3350",
+    "https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a",
+    "https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0",
+    "https://bcr.bazel.build/modules/gazelle/0.44.0/MODULE.bazel": "fd3177ca0938da57a1e416cad3f39b9c4334defbc717e89aba9d9ddbbb0341da",
+    "https://bcr.bazel.build/modules/gazelle/0.44.0/source.json": "7fb65ef9c1ce470d099ca27fd478673d9d64c844af28d0d472b0874c7d590cb6",
+    "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
+    "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
+    "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
+    "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/source.json": "41e9e129f80d8c8bf103a7acc337b76e54fad1214ac0a7084bf24f4cd924b8b4",
+    "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f",
+    "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075",
+    "https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d",
+    "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902",
+    "https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92",
+    "https://bcr.bazel.build/modules/package_metadata/0.0.2/source.json": "e53a759a72488d2c0576f57491ef2da0cf4aab05ac0997314012495935531b73",
+    "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
+    "https://bcr.bazel.build/modules/platforms/0.0.10/source.json": "f22828ff4cf021a6b577f1bf6341cb9dcd7965092a439f64fc1bb3b7a5ae4bd5",
+    "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee",
+    "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37",
+    "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615",
+    "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814",
+    "https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d",
+    "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7",
+    "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c",
+    "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d",
+    "https://bcr.bazel.build/modules/protobuf/29.0-rc2.bcr.1/MODULE.bazel": "52f4126f63a2f0bbf36b99c2a87648f08467a4eaf92ba726bc7d6a500bbf770c",
+    "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df",
+    "https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e",
+    "https://bcr.bazel.build/modules/protobuf/29.0/source.json": "b857f93c796750eef95f0d61ee378f3420d00ee1dd38627b27193aa482f4f981",
+    "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0",
+    "https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573",
+    "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858",
+    "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e",
+    "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/source.json": "be4789e951dd5301282729fe3d4938995dc4c1a81c2ff150afc9f1b0504c6022",
+    "https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206",
+    "https://bcr.bazel.build/modules/re2/2023-09-01/source.json": "e044ce89c2883cd957a2969a43e79f7752f9656f6b20050b62f90ede21ec6eb4",
+    "https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8",
+    "https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.14/MODULE.bazel": "5e343a3aac88b8d7af3b1b6d2093b55c347b8eefc2e7d1442f7a02dc8fea48ac",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.17/source.json": "4db99b3f55c90ab28d14552aa0632533e3e8e5e9aea0f5c24ac0014282c2a7c5",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
+    "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5",
+    "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
+    "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8",
+    "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/source.json": "c8b1e2c717646f1702290959a3302a178fb639d987ab61d548105019f11e527e",
+    "https://bcr.bazel.build/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8",
+    "https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270",
+    "https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd",
+    "https://bcr.bazel.build/modules/rules_go/0.51.0/MODULE.bazel": "b6920f505935bfd69381651c942496d99b16e2a12f3dd5263b90ded16f3b4d0f",
+    "https://bcr.bazel.build/modules/rules_go/0.55.1/MODULE.bazel": "a57a6fc59a74326c0b440d07cca209edf13c7d1a641e48cfbeab56e79f873609",
+    "https://bcr.bazel.build/modules/rules_go/0.55.1/source.json": "827a740c8959c9d20616889e7746cde4dcc6ee80d25146943627ccea0736328f",
+    "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
+    "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
+    "https://bcr.bazel.build/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39",
+    "https://bcr.bazel.build/modules/rules_java/6.4.0/MODULE.bazel": "e986a9fe25aeaa84ac17ca093ef13a4637f6107375f64667a15999f77db6c8f6",
+    "https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31",
+    "https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a",
+    "https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6",
+    "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab",
+    "https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2",
+    "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
+    "https://bcr.bazel.build/modules/rules_java/8.11.0/MODULE.bazel": "c3d280bc5ff1038dcb3bacb95d3f6b83da8dd27bba57820ec89ea4085da767ad",
+    "https://bcr.bazel.build/modules/rules_java/8.11.0/source.json": "302b52a39259a85aa06ca3addb9787864ca3e03b432a5f964ea68244397e7544",
+    "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
+    "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909",
+    "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036",
+    "https://bcr.bazel.build/modules/rules_jvm_external/5.3/MODULE.bazel": "bf93870767689637164657731849fb887ad086739bd5d360d90007a581d5527d",
+    "https://bcr.bazel.build/modules/rules_jvm_external/6.1/MODULE.bazel": "75b5fec090dbd46cf9b7d8ea08cf84a0472d92ba3585b476f44c326eda8059c4",
+    "https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0",
+    "https://bcr.bazel.build/modules/rules_jvm_external/6.3/source.json": "6f5f5a5a4419ae4e37c35a5bb0a6ae657ed40b7abc5a5189111b47fcebe43197",
+    "https://bcr.bazel.build/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59",
+    "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
+    "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5",
+    "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0",
+    "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d",
+    "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c",
+    "https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb",
+    "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
+    "https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
+    "https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a",
+    "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
+    "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
+    "https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f",
+    "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73",
+    "https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2",
+    "https://bcr.bazel.build/modules/rules_proto/7.0.2/source.json": "1e5e7260ae32ef4f2b52fd1d0de8d03b606a44c91b694d2f1afb1d3b28a48ce1",
+    "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f",
+    "https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300",
+    "https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382",
+    "https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed",
+    "https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58",
+    "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
+    "https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7",
+    "https://bcr.bazel.build/modules/rules_python/0.40.0/source.json": "939d4bd2e3110f27bfb360292986bb79fd8dcefb874358ccd6cdaa7bda029320",
+    "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
+    "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b",
+    "https://bcr.bazel.build/modules/rules_shell/0.3.0/source.json": "c55ed591aa5009401ddf80ded9762ac32c358d2517ee7820be981e2de9756cf3",
+    "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
+    "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
+    "https://bcr.bazel.build/modules/stardoc/0.5.6/MODULE.bazel": "c43dabc564990eeab55e25ed61c07a1aadafe9ece96a4efabb3f8bf9063b71ef",
+    "https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c",
+    "https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7",
+    "https://bcr.bazel.build/modules/stardoc/0.7.1/source.json": "b6500ffcd7b48cd72c29bb67bcac781e12701cc0d6d55d266a652583cfcdab01",
+    "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
+    "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
+    "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
+    "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79",
+    "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d",
+    "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198"
+  },
+  "selectedYankedVersions": {},
+  "moduleExtensions": {
+    "@@platforms//host:extension.bzl%host_platform": {
+      "general": {
+        "bzlTransitiveDigest": "xelQcPZH8+tmuOHVjL9vDxMnnQNMlwj0SlvgoqBkm4U=",
+        "usagesDigest": "SeQiIN/f8/Qt9vYQk7qcXp4I4wJeEC0RnQDiaaJ4tb8=",
+        "recordedFileInputs": {},
+        "recordedDirentsInputs": {},
+        "envVariables": {},
+        "generatedRepoSpecs": {
+          "host_platform": {
+            "repoRuleId": "@@platforms//host:extension.bzl%host_platform_repo",
+            "attributes": {}
+          }
+        },
+        "recordedRepoMappingEntries": []
+      }
+    },
+    "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
+      "general": {
+        "bzlTransitiveDigest": "sFhcgPbDQehmbD1EOXzX4H1q/CD5df8zwG4kp4jbvr8=",
+        "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=",
+        "recordedFileInputs": {},
+        "recordedDirentsInputs": {},
+        "envVariables": {},
+        "generatedRepoSpecs": {
+          "com_github_jetbrains_kotlin_git": {
+            "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_compiler_git_repository",
+            "attributes": {
+              "urls": [
+                "https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip"
+              ],
+              "sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88"
+            }
+          },
+          "com_github_jetbrains_kotlin": {
+            "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_capabilities_repository",
+            "attributes": {
+              "git_repository_name": "com_github_jetbrains_kotlin_git",
+              "compiler_version": "1.9.23"
+            }
+          },
+          "com_github_google_ksp": {
+            "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:ksp.bzl%ksp_compiler_plugin_repository",
+            "attributes": {
+              "urls": [
+                "https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip"
+              ],
+              "sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d",
+              "strip_version": "1.9.23-1.0.20"
+            }
+          },
+          "com_github_pinterest_ktlint": {
+            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_file",
+            "attributes": {
+              "sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985",
+              "urls": [
+                "https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint"
+              ],
+              "executable": true
+            }
+          },
+          "rules_android": {
+            "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
+            "attributes": {
+              "sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806",
+              "strip_prefix": "rules_android-0.1.1",
+              "urls": [
+                "https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip"
+              ]
+            }
+          }
+        },
+        "recordedRepoMappingEntries": [
+          [
+            "rules_kotlin+",
+            "bazel_tools",
+            "bazel_tools"
+          ]
+        ]
+      }
+    }
+  }
+}
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..527ad87
--- /dev/null
+++ b/README.md
@@ -0,0 +1,69 @@
+# osquery extensions
+
+Exactly as the name suggests, this is a bunch of table extensions for osquery.
+
+## How to build
+
+```
+bazel build //cmd/...
+```
+
+## How to run
+
+```
+bazel run //cmd/NAME -- --socket=/path/to/osquery_extensions.sock
+```
+
+## Plugins
+
+### `pacman`
+
+Provides the `pacman_packages` and `pacman_files` tables.
+
+Schema:
+
+```
+osquery> .schema pacman_packages
+CREATE TABLE pacman_packages(
+    `name` TEXT,
+    `version` TEXT,
+    `description` TEXT,
+    `arch` TEXT,
+    `url` TEXT,
+    `license` TEXT,
+    `size` BIGINT,
+    `explicit` INTEGER
+);
+
+osquery> .schema pacman_files
+CREATE TABLE pacman_files(
+    `package` TEXT,
+    `path` TEXT,
+    `size` BIGINT
+);
+```
+
+### `flatpak`
+
+Provides the `flatpak_packages` table.
+
+Schema:
+
+```
+osquery> .schema flatpak_packages
+CREATE TABLE flatpak_packages(
+    `id` TEXT,
+    `type` TEXT,
+    `name` TEXT,
+    `version` TEXT,
+    `hash` TEXT,
+    `branch` TEXT,
+    `user` TEXT
+);
+```
+
+## Author/License
+
+Written by Dan Fuhry <dan@fuhry.com>
+
+License: [BSD 3-clause](LICENSE)
diff --git a/cmd/flatpak/BUILD.bazel b/cmd/flatpak/BUILD.bazel
new file mode 100644 (file)
index 0000000..74bec19
--- /dev/null
@@ -0,0 +1,18 @@
+load("@rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "flatpak_lib",
+    srcs = ["main.go"],
+    importpath = "go.fuhry.dev/osquery/flatpak/cmd/flatpak",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//extcommon",
+        "//flatpak",
+    ],
+)
+
+go_binary(
+    name = "flatpak",
+    embed = [":flatpak_lib"],
+    visibility = ["//visibility:public"],
+)
diff --git a/cmd/flatpak/main.go b/cmd/flatpak/main.go
new file mode 100644 (file)
index 0000000..3a7d64a
--- /dev/null
@@ -0,0 +1,10 @@
+package main
+
+import (
+       "go.fuhry.dev/osquery/extcommon"
+       "go.fuhry.dev/osquery/flatpak"
+)
+
+func main() {
+       extcommon.Main("flatpak_packages", flatpak.Schema, flatpak.Generate)
+}
diff --git a/cmd/pacman/BUILD.bazel b/cmd/pacman/BUILD.bazel
new file mode 100644 (file)
index 0000000..d5065b0
--- /dev/null
@@ -0,0 +1,18 @@
+load("@rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "pacman_lib",
+    srcs = ["main.go"],
+    importpath = "go.fuhry.dev/osquery/pacman/cmd/pacman",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//extcommon",
+        "//pacman",
+    ],
+)
+
+go_binary(
+    name = "pacman",
+    embed = [":pacman_lib"],
+    visibility = ["//visibility:public"],
+)
diff --git a/cmd/pacman/main.go b/cmd/pacman/main.go
new file mode 100644 (file)
index 0000000..e7fa616
--- /dev/null
@@ -0,0 +1,15 @@
+package main
+
+import (
+       "go.fuhry.dev/osquery/extcommon"
+       "go.fuhry.dev/osquery/pacman"
+)
+
+func main() {
+       extcommon.MainMulti(
+               "pacman",
+               extcommon.Tables{
+                       "pacman_packages": {pacman.PackagesSchema, pacman.PackagesGenerate},
+                       "pacman_files":    {pacman.FilesSchema, pacman.FilesGenerate},
+               })
+}
diff --git a/extcommon/BUILD.bazel b/extcommon/BUILD.bazel
new file mode 100644 (file)
index 0000000..5491761
--- /dev/null
@@ -0,0 +1,17 @@
+load("@rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "extcommon",
+    srcs = [
+        "extcommon.go",
+        "util.go",
+    ],
+    importpath = "go.fuhry.dev/osquery/extcommon",
+    visibility = ["//visibility:public"],
+    deps = [
+        "@com_github_gobwas_glob//:glob",
+        "@com_github_hashicorp_golang_lru_v2//:golang-lru",
+        "@com_github_osquery_osquery_go//:osquery-go",
+        "@com_github_osquery_osquery_go//plugin/table",
+    ],
+)
diff --git a/extcommon/extcommon.go b/extcommon/extcommon.go
new file mode 100644 (file)
index 0000000..8d82a01
--- /dev/null
@@ -0,0 +1,53 @@
+package extcommon
+
+import (
+       "context"
+       "flag"
+       "log"
+
+       "github.com/osquery/osquery-go"
+       "github.com/osquery/osquery-go/plugin/table"
+)
+
+type SchemaFunc = func() []table.ColumnDefinition
+type GenerateFunc = func(context.Context, table.QueryContext) ([]map[string]string, error)
+type Tables map[string]struct {
+       Schema   SchemaFunc
+       Generate GenerateFunc
+}
+
+func Main(name string, s SchemaFunc, g GenerateFunc) {
+       MainMulti(name, Tables{name: {s, g}})
+}
+
+func MainMulti(pluginName string, t Tables) {
+       socket := flag.String("socket", "/opt/fleet-orbit/orbit-osquery.em", "path to osquery extensions socket")
+       flag.Parse()
+
+       if *socket == "" {
+               log.Fatal("please specify path to the osquery extensions socket")
+       }
+
+       server, err := osquery.NewExtensionManagerServer(pluginName, *socket)
+       if err != nil {
+               log.Fatalf("failed to connect to osquery socket: %v", err)
+       }
+
+       for name, t := range t {
+               server.RegisterPlugin(table.NewPlugin(name, t.Schema(), wrapGenerate(name, t.Generate)))
+       }
+       log.Printf("running server for plugin %q", pluginName)
+       if err := server.Run(); err != nil {
+               log.Fatalf("failed running server: %v", err)
+       }
+}
+
+func wrapGenerate(name string, g GenerateFunc) GenerateFunc {
+       return func(ctx context.Context, q table.QueryContext) ([]map[string]string, error) {
+               out, err := g(ctx, q)
+               if err != nil {
+                       log.Printf("error when querying plugin %q: %v", name, err)
+               }
+               return out, err
+       }
+}
diff --git a/extcommon/util.go b/extcommon/util.go
new file mode 100644 (file)
index 0000000..b3ed653
--- /dev/null
@@ -0,0 +1,79 @@
+package extcommon
+
+import (
+       "log"
+       "os/user"
+       "regexp"
+
+       "github.com/gobwas/glob"
+       lru "github.com/hashicorp/golang-lru/v2"
+)
+
+const lruSize = 128
+
+type cacheKey struct {
+       pattern    string
+       separators string
+}
+
+func newCacheKey(pattern string, separators []rune) cacheKey {
+       var s string
+       for _, c := range separators {
+               s += string(c)
+       }
+       return cacheKey{pattern, s}
+}
+
+var globCache *lru.Cache[cacheKey, glob.Glob]
+var regexpCache *lru.Cache[string, *regexp.Regexp]
+
+// CompileGlob compiles a glob, using the LRU cache to retrieve a previously compiled pattern if
+// possible.
+func CompileGlob(pattern string, separators ...rune) (glob.Glob, error) {
+       k := newCacheKey(pattern, separators)
+
+       if g, ok := globCache.Get(k); ok {
+               return g, nil
+       }
+
+       g, err := glob.Compile(pattern, separators...)
+       if err != nil {
+               return nil, err
+       }
+       globCache.Add(k, g)
+       return g, nil
+}
+
+// CompileRegexp compiles a regexp, using the LRU cache to retrieve a previously compiled pattern if
+// possible.
+func CompileRegexp(pattern string) (*regexp.Regexp, error) {
+       if r, ok := regexpCache.Get(pattern); ok {
+               return r, nil
+       }
+
+       r, err := regexp.Compile(pattern)
+       if err != nil {
+               return nil, err
+       }
+       regexpCache.Add(pattern, r)
+       return r, nil
+}
+
+// ListUsers lists local user accounts.
+func ListUsers() ([]*user.User, error) {
+       return nil, nil
+}
+
+func init() {
+       var err error
+
+       globCache, err = lru.New[cacheKey, glob.Glob](lruSize)
+       if err != nil {
+               log.Fatal(err)
+       }
+
+       regexpCache, err = lru.New[string, *regexp.Regexp](lruSize)
+       if err != nil {
+               log.Fatal(err)
+       }
+}
diff --git a/flatpak/BUILD.bazel b/flatpak/BUILD.bazel
new file mode 100644 (file)
index 0000000..69d9d81
--- /dev/null
@@ -0,0 +1,17 @@
+load("@rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "flatpak",
+    srcs = [
+        "data.go",
+        "plugin.go",
+        "registry.go",
+    ],
+    importpath = "go.fuhry.dev/osquery/flatpak",
+    visibility = ["//visibility:public"],
+    deps = [
+        "@com_github_chrisportman_go_gvariant//gvariant",
+        "@com_github_linuxdeepin_go_lib//users/passwd",
+        "@com_github_osquery_osquery_go//plugin/table",
+    ],
+)
diff --git a/flatpak/data.go b/flatpak/data.go
new file mode 100644 (file)
index 0000000..af5b6ec
--- /dev/null
@@ -0,0 +1,152 @@
+package flatpak
+
+import (
+       "bytes"
+       "encoding/binary"
+       "errors"
+       "fmt"
+
+       "github.com/chrisportman/go-gvariant/gvariant"
+)
+
+type DeployData_Metadata map[string]gvariant.Variant
+
+// struct for gvariant string "(ssasta{sv})"
+// source: https://github.com/flatpak/flatpak/blob/0152272d6caf2622536fad8869573a76001a493b/common/flatpak-dir-private.h#L346
+// FLATPAK_DEPLOY_DATA_GVARIANT_FORMAT:
+// s - origin
+// s - commit
+// as - subpaths
+// t - installed size
+// a{sv} - Metadata
+type DeployData struct {
+       Origin        string
+       Commit        string
+       Subpaths      []string
+       InstalledSize uint64
+       Metadata      []DeployData_Metadata
+}
+
+const (
+       TypeBool   = "b"
+       TypeInt8   = "y"
+       TypeInt16  = "n"
+       TypeUint16 = "q"
+       TypeInt32  = "i"
+       TypeUint32 = "u"
+       TypeInt64  = "x"
+       TypeUint64 = "t"
+       TypeFloat  = "d"
+       TypeString = "s"
+
+       kAppName         = "appdata-name"
+       kAppVersion      = "appdata-version"
+       kAppSummary      = "appdata-summary"
+       kRuntime         = "runtime"
+       kContentRating   = "appdata-content-rating"
+       kMetadataVersion = "deploy-version"
+       kTimestamp       = "timestamp"
+       kLicense         = "appdata-license"
+)
+
+var ErrSize = errors.New("variant's data field is the wrong size for the specified type")
+
+// VariantValue decodes the Data field of a Variant to the native Go type.
+func VariantValue(v *gvariant.Variant, endianness binary.ByteOrder) (any, error) {
+       if v == nil {
+               return nil, errors.New("variant is nil")
+       }
+       switch v.Format {
+       case TypeBool:
+               return len(v.Data) > 0 && v.Data[0] > 0, nil
+       case TypeInt8:
+               if len(v.Data) != 1 {
+                       return nil, ErrSize
+               }
+               return int8(v.Data[0]), nil
+       case TypeInt16:
+               if len(v.Data) != 2 {
+                       return nil, ErrSize
+               }
+               return int16(endianness.Uint16(v.Data)), nil
+       case TypeUint16:
+               if len(v.Data) != 2 {
+                       return nil, ErrSize
+               }
+               return endianness.Uint16(v.Data), nil
+       case TypeInt32:
+               if len(v.Data) != 4 {
+                       return nil, ErrSize
+               }
+               return int32(endianness.Uint32(v.Data)), nil
+       case TypeUint32:
+               if len(v.Data) != 4 {
+                       return nil, ErrSize
+               }
+               return endianness.Uint32(v.Data), nil
+       case TypeInt64:
+               if len(v.Data) != 8 {
+                       return nil, ErrSize
+               }
+               return int64(endianness.Uint64(v.Data)), nil
+       case TypeUint64:
+               if len(v.Data) != 8 {
+                       return nil, ErrSize
+               }
+               return endianness.Uint64(v.Data), nil
+       case TypeFloat:
+               switch len(v.Data) {
+               case 4:
+                       var out float32
+                       buf := bytes.NewBuffer(v.Data)
+                       if err := binary.Read(buf, endianness, &out); err != nil {
+                               return nil, err
+                       }
+                       return out, nil
+               case 8:
+                       var out float64
+                       buf := bytes.NewBuffer(v.Data)
+                       if err := binary.Read(buf, endianness, &out); err != nil {
+                               return nil, err
+                       }
+                       return out, nil
+               default:
+                       return nil, ErrSize
+               }
+       case TypeString:
+               return string(v.Data), nil
+       }
+
+       return nil, fmt.Errorf("unsupported format: %q", v.Format)
+}
+
+// Keys returns the list of all metadata keys
+func (d *DeployData) Keys() (out []string) {
+       for _, m := range d.Metadata {
+               for k, _ := range m {
+                       out = append(out, k)
+               }
+       }
+       return
+}
+
+// GetMetadata searches the metadata of a DeployData struct for the given key.
+func (d *DeployData) GetMetadata(key string) (*gvariant.Variant, bool) {
+       for _, m := range d.Metadata {
+               if v, ok := m[key]; ok {
+                       return &v, ok
+               }
+       }
+       return nil, false
+}
+
+// LoadDeployData parses and loads the binary `deploy` file.
+func LoadDeployData(contents []byte) (*DeployData, error) {
+       out := &DeployData{}
+       err := gvariant.UnmarshalBigEndian(contents, out)
+       if err != nil {
+               return nil, err
+       }
+
+       return out, nil
+}
diff --git a/flatpak/plugin.go b/flatpak/plugin.go
new file mode 100644 (file)
index 0000000..2d6504f
--- /dev/null
@@ -0,0 +1,44 @@
+package flatpak
+
+import (
+       "context"
+
+       "github.com/osquery/osquery-go/plugin/table"
+)
+
+const (
+       ColumnID      = "id"
+       ColumnType    = "type"
+       ColumnName    = "name"
+       ColumnVersion = "version"
+       ColumnHash    = "hash"
+       ColumnBranch  = "branch"
+       ColumnUser    = "user"
+)
+
+func Schema() (out []table.ColumnDefinition) {
+       return []table.ColumnDefinition{
+               table.TextColumn(ColumnID),
+               table.TextColumn(ColumnType),
+               table.TextColumn(ColumnName),
+               table.TextColumn(ColumnVersion),
+               table.TextColumn(ColumnHash),
+               table.TextColumn(ColumnBranch),
+               table.TextColumn(ColumnUser),
+       }
+}
+
+func Generate(ctx context.Context, q table.QueryContext) (out []map[string]string, err error) {
+       for _, pkg := range Packages() {
+               out = append(out, map[string]string{
+                       ColumnID:      pkg.Id(),
+                       ColumnType:    string(pkg.Type()),
+                       ColumnName:    pkg.Name(),
+                       ColumnVersion: pkg.Version(),
+                       ColumnHash:    pkg.Hash(),
+                       ColumnBranch:  pkg.Branch(),
+                       ColumnUser:    pkg.User(),
+               })
+       }
+       return out, err
+}
diff --git a/flatpak/registry.go b/flatpak/registry.go
new file mode 100644 (file)
index 0000000..f60fa10
--- /dev/null
@@ -0,0 +1,329 @@
+package flatpak
+
+import (
+       "encoding/binary"
+       "errors"
+       "flag"
+       "fmt"
+       "log"
+       "os"
+       "os/user"
+       "path"
+       "regexp"
+       "strings"
+
+       "github.com/linuxdeepin/go-lib/users/passwd"
+)
+
+type PackageType string
+
+type IPackage interface {
+       Id() string
+       Name() string
+       Version() string
+       Architecture() string
+       Branch() string
+       Hash() string
+       Type() PackageType
+       User() string
+}
+
+type packagePrimitive struct {
+       id     string
+       user   string
+       t      PackageType
+       arch   string
+       branch string
+       hash   string
+}
+
+type ArchitectureBranch struct {
+       Architecture string
+       Branch       string
+}
+
+const (
+       TypeApp     PackageType = "app"
+       TypeRuntime PackageType = "runtime"
+)
+
+const (
+       SymlinkCurrentArchitecture = "current"
+       SymlinkActiveHash          = "active"
+)
+
+var (
+       systemLocation = "/var/lib/flatpak"
+       userLocation   = ".local/share/flatpak"
+
+       subpaths = []PackageType{
+               TypeApp,
+               TypeRuntime,
+       }
+       applicationIdRegexp = regexp.MustCompile(`^([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)(\.([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?))*$`)
+)
+
+func Packages() (out []IPackage) {
+       type scanLocation struct {
+               baseDir string
+               user    string
+       }
+
+       scanLocations := []scanLocation{
+               {systemLocation, ""},
+       }
+
+       for _, entry := range passwd.GetPasswdEntry() {
+               userDir := path.Join(entry.Home, userLocation)
+               if st, err := os.Stat(userDir); err == nil && st.IsDir() {
+                       scanLocations = append(scanLocations, scanLocation{userDir, entry.Name})
+               }
+       }
+
+       for _, loc := range scanLocations {
+               for _, sub := range subpaths {
+                       dir := path.Join(loc.baseDir, string(sub))
+                       if entries, err := os.ReadDir(dir); err == nil {
+                               for _, entry := range entries {
+                                       if !entry.IsDir() || !applicationIdRegexp.MatchString(entry.Name()) {
+                                               continue
+                                       }
+                                       pp := &packagePrimitive{
+                                               id:   entry.Name(),
+                                               user: loc.user,
+                                               t:    sub,
+                                       }
+
+                                       if ab, err := pp.architecturesAndBranches(); err == nil {
+                                               for _, ab := range ab {
+                                                       out = append(out, pp.WithArchBranch(ab.Architecture, ab.Branch))
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+
+       return
+}
+
+// Id implements IPackage
+func (pp *packagePrimitive) Id() string {
+       return pp.id
+}
+
+// Name implements IPackage
+func (pp *packagePrimitive) Name() string {
+       return pp.getMetadataString(kAppName)
+}
+
+// Version implements IPackage
+func (pp *packagePrimitive) Version() string {
+       return pp.getMetadataString(kAppVersion)
+}
+
+func (pp *packagePrimitive) getMetadataString(k string) string {
+       deploy, err := pp.parseDeployFile()
+       if err != nil {
+               log.Printf("failed to parseDeployFile: %+v", err)
+               return ""
+       }
+
+       if key, ok := deploy.GetMetadata(k); ok {
+               if v, err := VariantValue(key, binary.BigEndian); err == nil {
+                       if name, ok := v.(string); ok {
+                               return name
+                       }
+               }
+       }
+
+       return ""
+}
+
+// Architecture implements IPackage
+func (pp *packagePrimitive) Architecture() string {
+       a, _, _ := pp.currentArchitectureAndBranch()
+       return a
+}
+
+// Branch implements IPackage
+func (pp *packagePrimitive) Branch() string {
+       b, _, _ := pp.currentArchitectureAndBranch()
+       return b
+}
+
+// Hash implements IPackage
+func (pp *packagePrimitive) Hash() string {
+       h, _ := pp.activeHash()
+       return h
+}
+
+// Type implements IPackage
+func (pp *packagePrimitive) Type() PackageType {
+       return pp.t
+}
+
+// User implements IPackage
+func (pp *packagePrimitive) User() string {
+       return pp.user
+}
+
+func (pp *packagePrimitive) dir() (string, error) {
+       if pp.user == "" {
+               return path.Join(systemLocation, string(pp.t), pp.id), nil
+       }
+
+       u, err := user.Lookup(pp.user)
+       if err != nil {
+               return "", err
+       }
+
+       return path.Join(u.HomeDir, userLocation, string(pp.t), pp.id), nil
+}
+
+func (pp *packagePrimitive) currentArchitectureAndBranch() (string, string, error) {
+       if pp.arch != "" && pp.branch != "" {
+               return pp.arch, pp.branch, nil
+       }
+
+       dir, err := pp.dir()
+       if err != nil {
+               return "", "", err
+       }
+
+       if link, err := os.Readlink(path.Join(dir, SymlinkCurrentArchitecture)); err == nil {
+               parts := strings.Split(link, string(os.PathSeparator))
+               if len(parts) == 2 {
+                       pp.arch = parts[0]
+                       pp.branch = parts[1]
+               } else {
+                       return "", "", fmt.Errorf("invalid format of %q symlink: %s: expected arch/branch",
+                               SymlinkCurrentArchitecture, link)
+               }
+       } else {
+               archs, err := subdirs(dir)
+               if err != nil {
+                       return "", "", fmt.Errorf("failed to list architectures for package %s: %v", pp.id, err)
+               }
+               if len(archs) != 1 {
+                       return "", "", fmt.Errorf("package %s does not have exactly 1 architecture", pp.id)
+               }
+
+               pp.arch = archs[0]
+
+               branches, err := subdirs(path.Join(dir, archs[0]))
+               if err != nil {
+                       return "", "", fmt.Errorf("failed to list branches for package %s and arch %s: %v", pp.id, archs[0], err)
+               }
+
+               if len(branches) != 1 {
+                       return "", "", fmt.Errorf("package %s does not have exactly 1 branch for arch %s", pp.id, archs[0])
+               }
+
+               pp.branch = branches[0]
+       }
+
+       return pp.arch, pp.branch, err
+}
+
+func (pp *packagePrimitive) architecturesAndBranches() (out []ArchitectureBranch, err error) {
+       dir, err := pp.dir()
+       if err != nil {
+               return
+       }
+
+       arches, err := subdirs(dir)
+       if err != nil {
+               return
+       }
+       for _, arch := range arches {
+               branches, err := subdirs(path.Join(dir, arch))
+               if err != nil {
+                       continue
+               }
+               for _, branch := range branches {
+                       out = append(out, ArchitectureBranch{arch, branch})
+               }
+       }
+
+       return
+}
+
+func (pp *packagePrimitive) WithArchBranch(architecture, branch string) *packagePrimitive {
+       npp := *pp
+       npp.arch = architecture
+       npp.branch = branch
+       return &npp
+}
+
+func (pp *packagePrimitive) activeHash() (string, error) {
+       if pp.hash != "" {
+               return pp.hash, nil
+       }
+
+       dir, err := pp.dir()
+       if err != nil {
+               return "", err
+       }
+
+       arch, branch, err := pp.currentArchitectureAndBranch()
+       if err != nil {
+               return "", err
+       }
+
+       hash, err := os.Readlink(path.Join(dir, arch, branch, SymlinkActiveHash))
+       pp.hash = hash
+       return hash, err
+}
+
+func (pp *packagePrimitive) parseDeployFile() (*DeployData, error) {
+       if pp.branch == "" {
+               return nil, errors.New("branch is not set")
+       }
+
+       dir, err := pp.dir()
+       if err != nil {
+               return nil, err
+       }
+
+       arch, branch, err := pp.currentArchitectureAndBranch()
+       if err != nil {
+               return nil, err
+       }
+
+       hash, err := pp.activeHash()
+       if err != nil {
+               return nil, err
+       }
+
+       deployPath := path.Join(dir, arch, branch, hash, "deploy")
+       contents, err := os.ReadFile(deployPath)
+       if err != nil {
+               return nil, err
+       }
+
+       return LoadDeployData(contents)
+}
+
+func subdirs(dir string) (out []string, err error) {
+       entries, err := os.ReadDir(dir)
+       if err != nil {
+               return
+       }
+
+       for _, entry := range entries {
+               if entry.IsDir() && entry.Name() != "." && entry.Name() != ".." {
+                       out = append(out, entry.Name())
+               }
+       }
+
+       return
+}
+
+func init() {
+       flag.StringVar(
+               &systemLocation,
+               "flatpak.system-dir",
+               systemLocation,
+               "directory where system-wide flatpak packages are installed")
+}
diff --git a/go.mod b/go.mod
new file mode 100644 (file)
index 0000000..1dc801f
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,24 @@
+module go.fuhry.dev/osquery
+
+go 1.24.4
+
+require (
+       github.com/Jguer/go-alpm/v2 v2.2.2
+       github.com/chrisportman/go-gvariant v0.0.4
+       github.com/gobwas/glob v0.2.3
+       github.com/hashicorp/golang-lru/v2 v2.0.7
+       github.com/linuxdeepin/go-lib v0.0.0-20250616085703-5b1a742a7af7
+       github.com/osquery/osquery-go v0.0.0-20250131154556-629f995b6947
+)
+
+require (
+       github.com/Microsoft/go-winio v0.6.2 // indirect
+       github.com/apache/thrift v0.20.0 // indirect
+       github.com/go-logr/logr v1.2.4 // indirect
+       github.com/go-logr/stdr v1.2.2 // indirect
+       github.com/pkg/errors v0.9.1 // indirect
+       go.opentelemetry.io/otel v1.16.0 // indirect
+       go.opentelemetry.io/otel/metric v1.16.0 // indirect
+       go.opentelemetry.io/otel/trace v1.16.0 // indirect
+       golang.org/x/sys v0.25.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644 (file)
index 0000000..2484d84
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,116 @@
+github.com/Jguer/go-alpm/v2 v2.2.2 h1:sPwUoZp1X5Tw6K6Ba1lWvVJfcgVNEGVcxARLBttZnC0=
+github.com/Jguer/go-alpm/v2 v2.2.2/go.mod h1:lfe8gSe83F/KERaQvEfrSqQ4n+8bES+ZIyKWR/gm3MI=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/Morganamilo/go-pacmanconf v0.0.0-20210502114700-cff030e927a5 h1:TMscPjkb1ThXN32LuFY5bEYIcXZx3YlwzhS1GxNpn/c=
+github.com/Morganamilo/go-pacmanconf v0.0.0-20210502114700-cff030e927a5/go.mod h1:Hk55m330jNiwxRodIlMCvw5iEyoRUCIY64W1p9D+tHc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
+github.com/apache/thrift v0.20.0 h1:631+KvYbsBZxmuJjYwhezVsrfc/TbqtZV4QcxOX1fOI=
+github.com/apache/thrift v0.20.0/go.mod h1:hOk1BQqcp2OLzGsyVXdfMk7YFlMxK3aoEVhjD06QhB8=
+github.com/chrisportman/go-gvariant v0.0.4 h1:LHcAoAuSFpTVsGHZCACO7K3xmiYHKrNl9Vq55TZW7N0=
+github.com/chrisportman/go-gvariant v0.0.4/go.mod h1:ZiNJGM0tEd/2iqFDXc6Xh29FCNbhxRuP+0uzQ4sP0vI=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/linuxdeepin/go-gir v0.0.0-20230331033513-a8d7a9e89f9b/go.mod h1:a0tox5vepTQu5iO6rdKc4diGT+fkyXZlRROM8ULEvaI=
+github.com/linuxdeepin/go-lib v0.0.0-20250616085703-5b1a742a7af7 h1:PdCjOLi1QOWF3s5nJpuv2nNmIjA2gRnVdRS8dVRQQcg=
+github.com/linuxdeepin/go-lib v0.0.0-20250616085703-5b1a742a7af7/go.mod h1:yNoMFao1mE45M8zW6i83eaGpT9eriCWsXEdcVNQswpw=
+github.com/linuxdeepin/go-x11-client v0.0.0-20220830090948-78fe92b727bb/go.mod h1:KwpmRZ47A/0a2l9V0V6aTlkuNaqy5j1fOqMFJONuIMY=
+github.com/mozillazg/go-pinyin v0.19.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
+github.com/osquery/osquery-go v0.0.0-20250131154556-629f995b6947 h1:EDgVELFaHiQXln+fZs9Ib9aXJwBEfa2qBZMVpSUYbYM=
+github.com/osquery/osquery-go v0.0.0-20250131154556-629f995b6947/go.mod h1:4cBOmXSmmDULG4bTOq0EFvIy5NUMNJMKbLDBMg6lhJE=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
+github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
+github.com/youpy/go-wav v0.3.2/go.mod h1:0FCieAXAeSdcxFfwLpRuEo0PFmAoc+8NU34h7TUvk50=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVYnsQOQ6L7P4a2ZxH47ixWcMXFGyx8=
+go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
+go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
+go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
+go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
+go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
+go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/image v0.10.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
+golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
diff --git a/pacman/BUILD.bazel b/pacman/BUILD.bazel
new file mode 100644 (file)
index 0000000..1272b65
--- /dev/null
@@ -0,0 +1,16 @@
+load("@rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "pacman",
+    srcs = [
+        "pacman.go",
+        "typed_columns.go",
+    ],
+    importpath = "go.fuhry.dev/osquery/pacman",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//extcommon",
+        "@com_github_jguer_go_alpm_v2//:go-alpm",
+        "@com_github_osquery_osquery_go//plugin/table",
+    ],
+)
diff --git a/pacman/pacman.go b/pacman/pacman.go
new file mode 100644 (file)
index 0000000..df0f4c6
--- /dev/null
@@ -0,0 +1,197 @@
+package pacman
+
+import (
+       "context"
+       "flag"
+       "fmt"
+       "strings"
+       "sync"
+
+       "github.com/Jguer/go-alpm/v2"
+       "github.com/osquery/osquery-go/plugin/table"
+)
+
+const (
+       ColumnName         = "name"
+       ColumnVersion      = "version"
+       ColumnDescription  = "description"
+       ColumnArchitecture = "arch"
+       ColumnUrl          = "url"
+       ColumnLicense      = "license"
+       ColumnSize         = "size"
+       ColumnExplicit     = "explicit"
+
+       ColumnPackage = "package"
+       ColumnPath    = "path"
+)
+
+var (
+       h      *alpm.Handle
+       hMu    sync.Mutex
+       dbPath string = "/var/lib/pacman"
+)
+
+var packagesColumns = []columnDef[alpm.IPackage]{
+       stringColumn[alpm.IPackage]{ColumnName, func(p alpm.IPackage) string { return p.Name() }},
+       stringColumn[alpm.IPackage]{ColumnVersion, func(p alpm.IPackage) string { return p.Version() }},
+       stringColumn[alpm.IPackage]{ColumnDescription, func(p alpm.IPackage) string { return p.Description() }},
+       stringColumn[alpm.IPackage]{ColumnArchitecture, func(p alpm.IPackage) string { return p.Architecture() }},
+       stringColumn[alpm.IPackage]{ColumnUrl, func(p alpm.IPackage) string { return p.URL() }},
+       stringColumn[alpm.IPackage]{ColumnLicense, func(p alpm.IPackage) string { return strings.Join(p.Licenses().Slice(), ",") }},
+       intColumn[alpm.IPackage]{ColumnSize, func(p alpm.IPackage) int64 { return p.ISize() }},
+       boolColumn[alpm.IPackage]{ColumnExplicit, func(p alpm.IPackage) bool { return p.Reason() == alpm.PkgReasonExplicit }},
+}
+
+type filesColumnsCtx = struct {
+       p alpm.IPackage
+       f alpm.File
+}
+
+var filesColumns = []columnDef[filesColumnsCtx]{
+       stringColumn[filesColumnsCtx]{ColumnPackage, func(c filesColumnsCtx) string { return c.p.Name() }},
+       stringColumn[filesColumnsCtx]{ColumnPath, func(c filesColumnsCtx) string { return c.f.Name }},
+       intColumn[filesColumnsCtx]{ColumnSize, func(c filesColumnsCtx) int64 { return c.f.Size }},
+}
+
+// PackagesSchema returns the schema for the "pacman_packages" table.
+func PackagesSchema() (out []table.ColumnDefinition) {
+       for _, c := range packagesColumns {
+               out = append(out, c.def())
+       }
+       return
+}
+
+// PackagesGenerate generates row data for the "pacman_packages" table.
+func PackagesGenerate(ctx context.Context, q table.QueryContext) ([]map[string]string, error) {
+       h, err := handle()
+       if err != nil {
+               return nil, err
+       }
+       defer release()
+
+       db, err := h.LocalDB()
+       if err != nil {
+               return nil, err
+       }
+
+       var out []map[string]string
+       err = db.PkgCache().ForEach(func(pkg alpm.IPackage) error {
+               row := make(map[string]string)
+               for _, col := range packagesColumns {
+                       if constraints, ok := q.Constraints[col.name()]; ok {
+                               if m, err := col.matches(pkg, constraints); !m {
+                                       return err
+                               }
+                       }
+                       rawValue := col.val(pkg)
+                       switch v := rawValue.(type) {
+                       case string:
+                               row[col.name()] = v
+                       case int, int64, uint, uint64:
+                               row[col.name()] = fmt.Sprintf("%d", v)
+                       case bool:
+                               row[col.name()] = "0"
+                               if v {
+                                       row[col.name()] = "1"
+                               }
+                       }
+               }
+               out = append(out, row)
+               return nil
+       })
+
+       return out, err
+}
+
+// FilesSchema returns the schema for the "pacman_files" table.
+func FilesSchema() (out []table.ColumnDefinition) {
+       for _, c := range filesColumns {
+               out = append(out, c.def())
+       }
+       return
+}
+
+// FilesGenerate generates row data for the "pacman_files" table.
+func FilesGenerate(ctx context.Context, q table.QueryContext) ([]map[string]string, error) {
+       h, err := handle()
+       if err != nil {
+               return nil, err
+       }
+       defer release()
+
+       db, err := h.LocalDB()
+       if err != nil {
+               return nil, err
+       }
+
+       var out []map[string]string
+       err = db.PkgCache().ForEach(func(pkg alpm.IPackage) error {
+               // filter on package name before iterating the files, which is computationally expensive
+               if c, ok := q.Constraints[ColumnPackage]; ok {
+                       if m, err := filesColumns[0].matches(filesColumnsCtx{pkg, alpm.File{}}, c); !m {
+                               return err
+                       }
+               }
+
+               for _, f := range pkg.Files() {
+                       row := make(map[string]string)
+                       for i, col := range filesColumns {
+                               ctx := filesColumnsCtx{pkg, f}
+                               // skip filtering on the `package` column, we did this above before iterating Files
+                               if i > 0 {
+                                       if constraints, ok := q.Constraints[col.name()]; ok {
+                                               if m, err := col.matches(ctx, constraints); !m {
+                                                       return err
+                                               }
+                                       }
+                               }
+                               rawValue := col.val(ctx)
+                               switch v := rawValue.(type) {
+                               case string:
+                                       row[col.name()] = v
+                               case int, int64, uint, uint64:
+                                       row[col.name()] = fmt.Sprintf("%d", v)
+                               case bool:
+                                       row[col.name()] = "0"
+                                       if v {
+                                               row[col.name()] = "1"
+                                       }
+                               }
+                       }
+                       out = append(out, row)
+               }
+               return nil
+       })
+
+       return out, err
+}
+
+func handle() (*alpm.Handle, error) {
+       var err error
+
+       hMu.Lock()
+       defer hMu.Unlock()
+
+       if h != nil {
+               return h, nil
+       }
+
+       h, err = alpm.Initialize("/", dbPath)
+       return h, err
+}
+
+func release() {
+       hMu.Lock()
+       defer hMu.Unlock()
+
+       if h == nil {
+               return
+       }
+
+       h.Release()
+       h = nil
+}
+
+func init() {
+       flag.StringVar(&dbPath, "pacman.db-path", dbPath, "path to pacman database")
+}
diff --git a/pacman/typed_columns.go b/pacman/typed_columns.go
new file mode 100644 (file)
index 0000000..74cce91
--- /dev/null
@@ -0,0 +1,168 @@
+package pacman
+
+import (
+       "fmt"
+       "strconv"
+       "strings"
+
+       "github.com/Jguer/go-alpm/v2"
+       "github.com/osquery/osquery-go/plugin/table"
+       "go.fuhry.dev/osquery/extcommon"
+)
+
+// TODO:
+// - make these interfaces public and move to extcommon
+// - support the LIKE operator
+
+type columnDef[T any] interface {
+       name() string
+       def() table.ColumnDefinition
+       val(ctx T) any
+       matches(T, table.ConstraintList) (bool, error)
+}
+
+type stringColumn[T any] struct {
+       n   string
+       get func(T) string
+}
+
+type intColumn[T any] struct {
+       n   string
+       get func(T) int64
+}
+
+type boolColumn[T any] struct {
+       n   string
+       get func(T) bool
+}
+
+func (sc stringColumn[T]) name() string { return sc.n }
+
+func (sc stringColumn[T]) val(ctx T) any {
+       return sc.get(ctx)
+}
+
+func (sc stringColumn[T]) def() table.ColumnDefinition {
+       return table.TextColumn(sc.name())
+}
+
+func (sc stringColumn[T]) matches(ctx T, constraints table.ConstraintList) (bool, error) {
+       if constraints.Affinity != table.ColumnTypeText {
+               return false, fmt.Errorf("unable to process constraint: column %q is a text column", sc.name())
+       }
+       val := sc.get(ctx)
+       for _, c := range constraints.Constraints {
+               switch c.Operator {
+               case table.OperatorEquals:
+                       return val == c.Expression, nil
+               case table.OperatorGreaterThan:
+                       return alpm.VerCmp(val, c.Expression) < 0, nil
+               case table.OperatorGreaterThanOrEquals:
+                       return val == c.Expression || alpm.VerCmp(val, c.Expression) < 0, nil
+               case table.OperatorLessThan:
+                       return alpm.VerCmp(c.Expression, val) < 0, nil
+               case table.OperatorLessThanOrEquals:
+                       return val == c.Expression || alpm.VerCmp(c.Expression, val) < 0, nil
+               case table.OperatorGlob:
+                       g, err := extcommon.CompileGlob(c.Expression)
+                       if err != nil {
+                               return false, err
+                       }
+                       return g.Match(val), nil
+               case table.OperatorRegexp:
+                       r, err := extcommon.CompileRegexp(c.Expression)
+                       if err != nil {
+                               return false, err
+                       }
+                       return r.MatchString(val), nil
+               default:
+                       return false, fmt.Errorf("unsupported operator: %v", c.Operator)
+               }
+       }
+
+       // if no constraints, return true
+       return true, nil
+}
+
+func (ic intColumn[T]) name() string { return ic.n }
+
+func (ic intColumn[T]) val(p T) any {
+       return ic.get(p)
+}
+
+func (ic intColumn[T]) def() table.ColumnDefinition {
+       return table.BigIntColumn(ic.name())
+}
+
+func (ic intColumn[T]) matches(ctx T, constraints table.ConstraintList) (bool, error) {
+       if constraints.Affinity != table.ColumnTypeInteger && constraints.Affinity != table.ColumnTypeBigInt {
+               return false, fmt.Errorf("unable to process constraint: column %q is an integer column", ic.name())
+       }
+       val := ic.get(ctx)
+       for _, c := range constraints.Constraints {
+               i, err := strconv.Atoi(c.Expression)
+               if err != nil {
+                       return false, err
+               }
+               exprInt := int64(i)
+               switch c.Operator {
+               case table.OperatorEquals:
+                       return val == exprInt, nil
+               case table.OperatorGreaterThan:
+                       return val > exprInt, nil
+               case table.OperatorGreaterThanOrEquals:
+                       return val >= exprInt, nil
+               case table.OperatorLessThan:
+                       return val < exprInt, nil
+               case table.OperatorLessThanOrEquals:
+                       return val <= exprInt, nil
+               default:
+                       return false, fmt.Errorf("unsupported operator: %v", c.Operator)
+               }
+       }
+
+       // if no constraints, return true
+       return true, nil
+}
+
+func (bc boolColumn[T]) name() string { return bc.n }
+
+func (bc boolColumn[T]) val(ctx T) any {
+       if bc.get(ctx) {
+               return 1
+       }
+       return 0
+}
+
+func (bc boolColumn[T]) def() table.ColumnDefinition {
+       return table.IntegerColumn(bc.name())
+}
+
+func (bc boolColumn[T]) matches(ctx T, constraints table.ConstraintList) (bool, error) {
+       val := bc.get(ctx)
+       for _, c := range constraints.Constraints {
+               e, err := parseTruthy(c.Expression)
+               if err != nil {
+                       return false, err
+               }
+               switch c.Operator {
+               case table.OperatorEquals:
+                       return val == e, nil
+               default:
+                       return false, fmt.Errorf("unsupported operator: %v", c.Operator)
+               }
+       }
+
+       // if no constraints, return true
+       return true, nil
+}
+
+func parseTruthy(val string) (bool, error) {
+       switch strings.ToLower(val) {
+       case "yes", "true", "1":
+               return true, nil
+       case "no", "false", "0":
+               return false, nil
+       }
+       return false, fmt.Errorf("not a truthy value: %q", val)
+}