Add adobe_plugins osquery extension table (#45208)
Closes #45206 ## Summary - Adds a new `adobe_plugins` osquery extension table to fleetd (macOS + Windows) - Parses CEP (`CSXS/manifest.xml`) and UXP (`manifest.json`) manifests for rich metadata - Falls back to filesystem info for native plug-ins where no manifest exists - Supports a `scan_level` WHERE constraint: `standard` (default) or `deep` ## Table schema | Column | Type | Description | | --- | --- | --- | | `path` | TEXT | Full path to the plugin directory or file | | `name` | TEXT | Plugin display name (from manifest or directory name) | | `version` | TEXT | Plugin version (from manifest) | | `vendor` | TEXT | Plugin author/publisher (from manifest) | | `bundle_id` | TEXT | Plugin bundle identifier (from manifest) | | `host_application` | TEXT | Target app(s): Photoshop, Illustrator, Premiere Pro, etc. | | `extension_type` | TEXT | `CEP`, `UXP`, or `native` | | `user` | TEXT | Username for user-scoped installs; empty for system-wide | | `platform` | TEXT | `darwin` or `windows` | | `scan_level` | TEXT | WHERE constraint only — `standard` (default) or `deep` | ## How I tested it > **Note:** Manual testing was done by installing two real open-source CEP extensions (downloaded from GitHub) on a macOS host without a full Adobe CC installation. This validates the table logic, manifest parsing, and osquery integration end-to-end against real-world manifest formats. **QA should test against machines with full Adobe Creative Cloud installations** (Photoshop, Premiere, Illustrator, etc.) to verify the scan paths match what Adobe actually ships, and to exercise `scan_level = 'deep'` with real native plug-in directories. Expect a few more dev cycles after QA feedback. ### 1. Unit tests — 22 passing ``` $ go test ./orbit/pkg/table/adobe_plugins/... -v --- PASS: TestParseCEPPlugin/valid_manifest --- PASS: TestParseCEPPlugin/missing_manifest_falls_back_to_dir_name --- PASS: TestParseCEPPlugin/malformed_manifest_falls_back_to_dir_name --- PASS: TestParseUXPPlugin/valid_manifest --- PASS: TestParseUXPPlugin/missing_manifest_falls_back_to_dir_name --- PASS: TestParseUXPPlugin/manifest_with_id_but_no_name_uses_id --- PASS: TestParseNativePlugin/* (5 subtests) --- PASS: TestResolveHostApps/* (7 subtests) --- PASS: TestScanEntry/* (2 subtests) PASS ``` ### 2. Cross-platform compilation ``` $ go build ./orbit/pkg/table/adobe_plugins/... # macOS ✅ $ GOOS=windows go build ./orbit/pkg/table/adobe_plugins/... # Windows ✅ $ GOOS=linux go build ./orbit/pkg/table/adobe_plugins/... # Linux stub ✅ $ go build ./orbit/cmd/fleetd_tables/ # Full fleetd binary ✅ $ go vet ./orbit/pkg/table/adobe_plugins/... # Clean ✅ ``` ### 3. Manual end-to-end testing on macOS (osquery 5.23.0) #### Setup Built the fleetd extension binary, then installed two **real open-source CEP extensions** from GitHub into the user-scoped scan path (`~/Library/Application Support/Adobe/CEP/extensions/`): 1. **[adobe-discord-rpc](https://github.com/Kuredew/adobe-discord-rpc)** — a real CEP extension targeting 11 Adobe apps. Has no `<Author>` element (tests missing-vendor edge case). Complex manifest with many host app codes. 2. **[cep-template](https://github.com/khanyuinc/cep-template)** — a CEP starter template targeting After Effects only. Minimal manifest. ```bash # Build extension go build -o build/fleetd-tables-test ./orbit/cmd/fleetd_tables/ # Install real extensions CEP_DIR="$HOME/Library/Application Support/Adobe/CEP/extensions" mkdir -p "$CEP_DIR/adobe-discord-rpc/CSXS" # downloaded CSXS/manifest.xml from GitHub into the directory mkdir -p "$CEP_DIR/cep-template/CSXS" # downloaded CSXS/manifest.xml from GitHub into the directory ``` #### Running the query ```bash OSQUERYD="/opt/orbit/bin/osqueryd/macos-app/stable/osquery.app/Contents/MacOS/osqueryd" $OSQUERYD -S --allow_unsafe --extensions_timeout=10 \ --extensions_require=com.fleetdm.fleetd_tables.osquery_extension.v1 \ --extension build/fleetd-tables-test \ --json "SELECT * FROM adobe_plugins;" ``` #### Actual output (verbatim) ```json [ { "bundle_id": "com.kureichi.discordrpc", "extension_type": "CEP", "host_application": "After Effects, Photoshop, Premiere Pro, InCopy, Audition, Dreamweaver, Animate, InDesign, Illustrator, Prelude", "name": "adobe-discord-rpc", "path": "/Users/sharonkatz/Library/Application Support/Adobe/CEP/extensions/adobe-discord-rpc", "platform": "darwin", "scan_level": "", "user": "sharonkatz", "vendor": "", "version": "3.1.1" }, { "bundle_id": "com.yourcompany", "extension_type": "CEP", "host_application": "After Effects", "name": "cep-template", "path": "/Users/sharonkatz/Library/Application Support/Adobe/CEP/extensions/cep-template", "platform": "darwin", "scan_level": "", "user": "sharonkatz", "vendor": "", "version": "1.0" } ] ``` #### osqueryi table output ``` +-------------------+---------+-------------------------+----------------------------------------------------------------------------------------------------------------+----------------+------------+ | name | version | bundle_id | host_application | extension_type | user | +-------------------+---------+-------------------------+----------------------------------------------------------------------------------------------------------------+----------------+------------+ | adobe-discord-rpc | 3.1.1 | com.kureichi.discordrpc | After Effects, Photoshop, Premiere Pro, InCopy, Audition, Dreamweaver, Animate, InDesign, Illustrator, Prelude | CEP | sharonkatz | | cep-template | 1.0 | com.yourcompany | After Effects | CEP | sharonkatz | +-------------------+---------+-------------------------+----------------------------------------------------------------------------------------------------------------+----------------+------------+ ``` #### What this verified | Scenario | Result | | --- | --- | | Real CEP manifest with 11 host apps | ✅ All codes resolved (AEFT→After Effects, PHSP/PHXS→Photoshop, PPRO→Premiere Pro, etc.) | | Missing `<Author>` element | ✅ `vendor` is empty string, no crash | | Minimal CEP manifest (single host) | ✅ `host_application=After Effects`, version/bundle_id correct | | User-scoped detection | ✅ `user=sharonkatz` populated | | Schema registration | ✅ `.schema adobe_plugins` shows all 10 columns | | No Adobe installed + no plugins | ✅ 0 rows, no error | | Deep scan with no app bundles | ✅ 0 extra rows, no error | ### Windows Not tested yet — Windows paths are implemented and cross-compile, but need manual verification on a Windows host with Adobe CC. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added Adobe plugins osquery table for macOS and Windows platforms * Discovers and catalogs Adobe CEP, UXP, and native plugins * Extracts plugin metadata including version, vendor, host applications, and installation paths * Supports configurable scan depth for comprehensive plugin discovery <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/45208) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Added new `adobe_plugins` osquery extension table to fleetd that detects Adobe CEP, UXP, and native plug-ins on macOS and Windows by scanning well-known directories and parsing plugin manifests for name, version, vendor, host application, and other metadata.
|
||||
@@ -0,0 +1,368 @@
|
||||
// Package adobe_plugins implements an osquery extension table that detects
|
||||
// Adobe plugins (CEP extensions, UXP extensions, and native plug-ins) on
|
||||
// macOS and Windows endpoints by scanning well-known directories and parsing
|
||||
// plugin manifests.
|
||||
//
|
||||
// The table supports a scan_level constraint in the WHERE clause:
|
||||
//
|
||||
// SELECT * FROM adobe_plugins; -- standard (default)
|
||||
// SELECT * FROM adobe_plugins WHERE scan_level = 'deep'; -- includes native plug-ins
|
||||
//
|
||||
// Standard: scans CEP and UXP extension directories only.
|
||||
// Deep: additionally scans application-specific native plug-in directories
|
||||
// (Photoshop, Premiere Pro, After Effects, Illustrator).
|
||||
package adobe_plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/table/tablehelpers"
|
||||
"github.com/osquery/osquery-go/plugin/table"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// maxManifestSize is the maximum size of a manifest file we'll read.
|
||||
// Prevents memory exhaustion from unexpectedly large files (orbit runs as root).
|
||||
const maxManifestSize = 1 << 20 // 1 MB
|
||||
|
||||
const tableName = "adobe_plugins"
|
||||
|
||||
const (
|
||||
colPath = "path"
|
||||
colName = "name"
|
||||
colVersion = "version"
|
||||
colVendor = "vendor"
|
||||
colBundleID = "bundle_id"
|
||||
colHostApplication = "host_application"
|
||||
colExtensionType = "extension_type"
|
||||
colUser = "user"
|
||||
colPlatform = "platform"
|
||||
colScanLevel = "scan_level"
|
||||
)
|
||||
|
||||
// scanPath describes a directory to scan for Adobe plugins.
|
||||
type scanPath struct {
|
||||
basePath string // directory path, may contain glob wildcards
|
||||
extensionType string // "CEP", "UXP", or "native"
|
||||
hostApp string // known host application from path context
|
||||
user string // username for user-scoped installs, empty for system
|
||||
}
|
||||
|
||||
// hostAppCodes maps Adobe host application codes found in manifests to
|
||||
// human-readable application names.
|
||||
var hostAppCodes = map[string]string{
|
||||
"PHXS": "Photoshop",
|
||||
"PHSP": "Photoshop",
|
||||
"PS": "Photoshop",
|
||||
"ILST": "Illustrator",
|
||||
"AI": "Illustrator",
|
||||
"PPRO": "Premiere Pro",
|
||||
"AEFT": "After Effects",
|
||||
"AE": "After Effects",
|
||||
"IDSN": "InDesign",
|
||||
"ID": "InDesign",
|
||||
"FLPR": "Animate",
|
||||
"DRWV": "Dreamweaver",
|
||||
"AUDT": "Audition",
|
||||
"AU": "Audition",
|
||||
"KBRG": "Bridge",
|
||||
"LTRM": "Lightroom",
|
||||
"LRCC": "Lightroom Classic",
|
||||
"XD": "XD",
|
||||
"AICY": "InCopy",
|
||||
"PRLD": "Prelude",
|
||||
}
|
||||
|
||||
type adobePluginsTable struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
// TablePlugin returns the osquery plugin for the adobe_plugins table.
|
||||
func TablePlugin(logger zerolog.Logger) *table.Plugin {
|
||||
t := &adobePluginsTable{
|
||||
logger: logger.With().Str("table", tableName).Logger(),
|
||||
}
|
||||
return table.NewPlugin(tableName, Columns(), t.generate)
|
||||
}
|
||||
|
||||
// Columns defines the table schema.
|
||||
func Columns() []table.ColumnDefinition {
|
||||
return []table.ColumnDefinition{
|
||||
table.TextColumn(colPath),
|
||||
table.TextColumn(colName),
|
||||
table.TextColumn(colVersion),
|
||||
table.TextColumn(colVendor),
|
||||
table.TextColumn(colBundleID),
|
||||
table.TextColumn(colHostApplication),
|
||||
table.TextColumn(colExtensionType),
|
||||
table.TextColumn(colUser),
|
||||
table.TextColumn(colPlatform),
|
||||
// scan_level controls scan depth. Populated in results so osquery's
|
||||
// post-generate WHERE filter doesn't discard rows.
|
||||
table.TextColumn(colScanLevel),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *adobePluginsTable) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
|
||||
scanLevels := tablehelpers.GetConstraints(queryContext, colScanLevel,
|
||||
tablehelpers.WithDefaults("standard"),
|
||||
tablehelpers.WithAllowedValues([]string{"standard", "deep"}),
|
||||
tablehelpers.WithLogger(t.logger),
|
||||
)
|
||||
|
||||
level := "standard"
|
||||
if slices.Contains(scanLevels, "deep") {
|
||||
level = "deep"
|
||||
}
|
||||
|
||||
paths, err := getScanPaths(level, t.logger)
|
||||
if err != nil {
|
||||
t.logger.Warn().Err(err).Msg("failed to build scan paths")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var results []map[string]string
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
for _, sp := range paths {
|
||||
if ctx.Err() != nil {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
matches, err := filepath.Glob(sp.basePath)
|
||||
if err != nil {
|
||||
t.logger.Debug().Err(err).Str("path", sp.basePath).Msg("glob error")
|
||||
continue
|
||||
}
|
||||
|
||||
for _, dir := range matches {
|
||||
if ctx.Err() != nil {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.logger.Debug().Err(err).Str("dir", dir).Msg("cannot read directory")
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
// Skip symlinks to avoid traversing outside intended scan dirs.
|
||||
if entry.Type()&fs.ModeSymlink != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
pluginPath := filepath.Join(dir, entry.Name())
|
||||
|
||||
if _, ok := seen[pluginPath]; ok {
|
||||
continue
|
||||
}
|
||||
seen[pluginPath] = struct{}{}
|
||||
|
||||
row := t.scanEntry(pluginPath, entry, sp)
|
||||
if row != nil {
|
||||
row[colScanLevel] = level
|
||||
results = append(results, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (t *adobePluginsTable) scanEntry(pluginPath string, entry os.DirEntry, sp scanPath) map[string]string {
|
||||
switch sp.extensionType {
|
||||
case "CEP":
|
||||
if !entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return t.parseCEPPlugin(pluginPath, sp)
|
||||
case "UXP":
|
||||
if !entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return t.parseUXPPlugin(pluginPath, sp)
|
||||
case "native":
|
||||
if strings.HasPrefix(entry.Name(), ".") {
|
||||
return nil
|
||||
}
|
||||
return parseNativePlugin(pluginPath, entry, sp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CEP manifest XML structures (CSXS/manifest.xml)
|
||||
|
||||
type cepManifest struct {
|
||||
XMLName xml.Name `xml:"ExtensionManifest"`
|
||||
BundleID string `xml:"ExtensionBundleId,attr"`
|
||||
Version string `xml:"ExtensionBundleVersion,attr"`
|
||||
Author struct {
|
||||
Name string `xml:"Name,attr"`
|
||||
} `xml:"Author"`
|
||||
ExecutionEnvironment struct {
|
||||
HostList struct {
|
||||
Hosts []struct {
|
||||
Name string `xml:"Name,attr"`
|
||||
} `xml:"Host"`
|
||||
} `xml:"HostList"`
|
||||
} `xml:"ExecutionEnvironment"`
|
||||
}
|
||||
|
||||
func (t *adobePluginsTable) parseCEPPlugin(pluginPath string, sp scanPath) map[string]string {
|
||||
row := map[string]string{
|
||||
colPath: pluginPath,
|
||||
colName: filepath.Base(pluginPath),
|
||||
colExtensionType: "CEP",
|
||||
colUser: sp.user,
|
||||
colPlatform: runtime.GOOS,
|
||||
}
|
||||
|
||||
manifestPath := filepath.Join(pluginPath, "CSXS", "manifest.xml")
|
||||
data, err := readFileCapped(manifestPath, maxManifestSize)
|
||||
if err != nil {
|
||||
t.logger.Debug().Err(err).Str("path", manifestPath).Msg("no CEP manifest found")
|
||||
return row
|
||||
}
|
||||
|
||||
var m cepManifest
|
||||
if err := xml.Unmarshal(data, &m); err != nil {
|
||||
t.logger.Debug().Err(err).Str("path", manifestPath).Msg("failed to parse CEP manifest")
|
||||
return row
|
||||
}
|
||||
|
||||
row[colVersion] = m.Version
|
||||
row[colVendor] = m.Author.Name
|
||||
row[colBundleID] = m.BundleID
|
||||
|
||||
var hostCodes []string
|
||||
for _, h := range m.ExecutionEnvironment.HostList.Hosts {
|
||||
hostCodes = append(hostCodes, h.Name)
|
||||
}
|
||||
hostApps := resolveHostApps(hostCodes)
|
||||
if hostApps == "" {
|
||||
hostApps = sp.hostApp
|
||||
}
|
||||
row[colHostApplication] = hostApps
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
// UXP manifest JSON structures (manifest.json)
|
||||
|
||||
type uxpManifest struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Host []struct {
|
||||
App string `json:"app"`
|
||||
} `json:"host"`
|
||||
Metadata struct {
|
||||
Publisher string `json:"publisher"`
|
||||
} `json:"metadata"`
|
||||
}
|
||||
|
||||
func (t *adobePluginsTable) parseUXPPlugin(pluginPath string, sp scanPath) map[string]string {
|
||||
row := map[string]string{
|
||||
colPath: pluginPath,
|
||||
colName: filepath.Base(pluginPath),
|
||||
colExtensionType: "UXP",
|
||||
colUser: sp.user,
|
||||
colPlatform: runtime.GOOS,
|
||||
}
|
||||
|
||||
manifestPath := filepath.Join(pluginPath, "manifest.json")
|
||||
data, err := readFileCapped(manifestPath, maxManifestSize)
|
||||
if err != nil {
|
||||
t.logger.Debug().Err(err).Str("path", manifestPath).Msg("no UXP manifest found")
|
||||
return row
|
||||
}
|
||||
|
||||
var m uxpManifest
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
t.logger.Debug().Err(err).Str("path", manifestPath).Msg("failed to parse UXP manifest")
|
||||
return row
|
||||
}
|
||||
|
||||
name := m.Name
|
||||
if name == "" {
|
||||
name = m.ID
|
||||
}
|
||||
if name == "" {
|
||||
name = filepath.Base(pluginPath)
|
||||
}
|
||||
row[colName] = name
|
||||
row[colVersion] = m.Version
|
||||
row[colVendor] = m.Metadata.Publisher
|
||||
row[colBundleID] = m.ID
|
||||
|
||||
var hostCodes []string
|
||||
for _, h := range m.Host {
|
||||
hostCodes = append(hostCodes, h.App)
|
||||
}
|
||||
hostApps := resolveHostApps(hostCodes)
|
||||
if hostApps == "" {
|
||||
hostApps = sp.hostApp
|
||||
}
|
||||
row[colHostApplication] = hostApps
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
func parseNativePlugin(pluginPath string, entry os.DirEntry, sp scanPath) map[string]string {
|
||||
name := entry.Name()
|
||||
for _, ext := range []string{".plugin", ".bundle", ".8bf", ".8bi", ".dll", ".aex"} {
|
||||
if strings.HasSuffix(strings.ToLower(name), ext) {
|
||||
name = name[:len(name)-len(ext)]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{
|
||||
colPath: pluginPath,
|
||||
colName: name,
|
||||
colHostApplication: sp.hostApp,
|
||||
colExtensionType: "native",
|
||||
colUser: sp.user,
|
||||
colPlatform: runtime.GOOS,
|
||||
}
|
||||
}
|
||||
|
||||
// readFileCapped reads up to maxBytes from a file. This prevents memory
|
||||
// exhaustion from unexpectedly large files since orbit runs as root.
|
||||
func readFileCapped(path string, maxBytes int64) ([]byte, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return io.ReadAll(io.LimitReader(f, maxBytes))
|
||||
}
|
||||
|
||||
// resolveHostApps converts a list of Adobe host application codes to
|
||||
// human-readable names, deduplicating entries.
|
||||
func resolveHostApps(codes []string) string {
|
||||
seen := make(map[string]struct{})
|
||||
var apps []string
|
||||
for _, code := range codes {
|
||||
app := code
|
||||
if resolved, ok := hostAppCodes[strings.ToUpper(code)]; ok {
|
||||
app = resolved
|
||||
}
|
||||
if _, exists := seen[app]; !exists {
|
||||
seen[app] = struct{}{}
|
||||
apps = append(apps, app)
|
||||
}
|
||||
}
|
||||
return strings.Join(apps, ", ")
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//go:build darwin
|
||||
|
||||
package adobe_plugins
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func getScanPaths(level string, logger zerolog.Logger) ([]scanPath, error) {
|
||||
var paths []scanPath
|
||||
|
||||
// System-wide CEP extensions
|
||||
paths = append(paths, scanPath{
|
||||
basePath: "/Library/Application Support/Adobe/CEP/extensions",
|
||||
extensionType: "CEP",
|
||||
})
|
||||
// System-wide UXP extensions
|
||||
paths = append(paths, scanPath{
|
||||
basePath: "/Library/Application Support/Adobe/UXP/extensions",
|
||||
extensionType: "UXP",
|
||||
})
|
||||
|
||||
// Per-user CEP and UXP extensions
|
||||
users, err := listLocalUsers()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to enumerate local users, skipping per-user paths")
|
||||
}
|
||||
for _, u := range users {
|
||||
paths = append(paths, scanPath{
|
||||
basePath: filepath.Join(u.homeDir, "Library", "Application Support", "Adobe", "CEP", "extensions"),
|
||||
extensionType: "CEP",
|
||||
user: u.name,
|
||||
})
|
||||
paths = append(paths, scanPath{
|
||||
basePath: filepath.Join(u.homeDir, "Library", "Application Support", "Adobe", "UXP", "extensions"),
|
||||
extensionType: "UXP",
|
||||
user: u.name,
|
||||
})
|
||||
}
|
||||
|
||||
if level == "deep" {
|
||||
paths = append(paths,
|
||||
scanPath{
|
||||
basePath: "/Applications/Adobe Photoshop */Plug-ins",
|
||||
extensionType: "native",
|
||||
hostApp: "Photoshop",
|
||||
},
|
||||
scanPath{
|
||||
basePath: "/Applications/Adobe Premiere Pro */Plug-ins",
|
||||
extensionType: "native",
|
||||
hostApp: "Premiere Pro",
|
||||
},
|
||||
scanPath{
|
||||
basePath: "/Applications/Adobe After Effects */Plug-ins",
|
||||
extensionType: "native",
|
||||
hostApp: "After Effects",
|
||||
},
|
||||
scanPath{
|
||||
basePath: "/Applications/Adobe Illustrator */Plug-ins",
|
||||
extensionType: "native",
|
||||
hostApp: "Illustrator",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
type localUser struct {
|
||||
name string
|
||||
homeDir string
|
||||
}
|
||||
|
||||
func listLocalUsers() ([]localUser, error) {
|
||||
entries, err := os.ReadDir("/Users")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var users []localUser
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
if strings.HasPrefix(name, ".") || name == "Shared" {
|
||||
continue
|
||||
}
|
||||
users = append(users, localUser{
|
||||
name: name,
|
||||
homeDir: filepath.Join("/Users", name),
|
||||
})
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build !darwin && !windows
|
||||
|
||||
package adobe_plugins
|
||||
|
||||
import "github.com/rs/zerolog"
|
||||
|
||||
// getScanPaths returns nil on unsupported platforms (Linux, etc.).
|
||||
// Adobe Creative Cloud does not run on Linux.
|
||||
func getScanPaths(_ string, _ zerolog.Logger) ([]scanPath, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
package adobe_plugins
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseCEPPlugin(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tbl := &adobePluginsTable{logger: zerolog.Nop()}
|
||||
|
||||
t.Run("valid manifest", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
pluginPath := filepath.Join("testdata", "cep_plugin")
|
||||
sp := scanPath{extensionType: "CEP"}
|
||||
|
||||
row := tbl.parseCEPPlugin(pluginPath, sp)
|
||||
require.NotNil(t, row)
|
||||
|
||||
assert.Equal(t, pluginPath, row[colPath])
|
||||
assert.Equal(t, "cep_plugin", row[colName])
|
||||
assert.Equal(t, "2.1.0", row[colVersion])
|
||||
assert.Equal(t, "Test Vendor", row[colVendor])
|
||||
assert.Equal(t, "com.example.test.plugin", row[colBundleID])
|
||||
assert.Contains(t, row[colHostApplication], "Photoshop")
|
||||
assert.Contains(t, row[colHostApplication], "Illustrator")
|
||||
assert.Equal(t, "CEP", row[colExtensionType])
|
||||
})
|
||||
|
||||
t.Run("missing manifest falls back to dir name", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
pluginPath := filepath.Join(dir, "no_manifest_plugin")
|
||||
require.NoError(t, os.MkdirAll(pluginPath, 0o755))
|
||||
|
||||
sp := scanPath{extensionType: "CEP", user: "testuser"}
|
||||
row := tbl.parseCEPPlugin(pluginPath, sp)
|
||||
require.NotNil(t, row)
|
||||
|
||||
assert.Equal(t, "no_manifest_plugin", row[colName])
|
||||
assert.Equal(t, "CEP", row[colExtensionType])
|
||||
assert.Equal(t, "testuser", row[colUser])
|
||||
assert.Empty(t, row[colVersion])
|
||||
assert.Empty(t, row[colBundleID])
|
||||
})
|
||||
|
||||
t.Run("malformed manifest falls back to dir name", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
pluginPath := filepath.Join(dir, "bad_manifest")
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(pluginPath, "CSXS"), 0o755))
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(pluginPath, "CSXS", "manifest.xml"),
|
||||
[]byte("not valid xml {{{"),
|
||||
0o644,
|
||||
))
|
||||
|
||||
sp := scanPath{extensionType: "CEP"}
|
||||
row := tbl.parseCEPPlugin(pluginPath, sp)
|
||||
require.NotNil(t, row)
|
||||
|
||||
assert.Equal(t, "bad_manifest", row[colName])
|
||||
assert.Equal(t, "CEP", row[colExtensionType])
|
||||
assert.Empty(t, row[colVersion])
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseUXPPlugin(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tbl := &adobePluginsTable{logger: zerolog.Nop()}
|
||||
|
||||
t.Run("valid manifest", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
pluginPath := filepath.Join("testdata", "uxp_plugin")
|
||||
sp := scanPath{extensionType: "UXP"}
|
||||
|
||||
row := tbl.parseUXPPlugin(pluginPath, sp)
|
||||
require.NotNil(t, row)
|
||||
|
||||
assert.Equal(t, pluginPath, row[colPath])
|
||||
assert.Equal(t, "Test UXP Plugin", row[colName])
|
||||
assert.Equal(t, "3.0.1", row[colVersion])
|
||||
assert.Equal(t, "UXP Test Vendor", row[colVendor])
|
||||
assert.Equal(t, "com.example.uxp.plugin", row[colBundleID])
|
||||
assert.Contains(t, row[colHostApplication], "Photoshop")
|
||||
assert.Contains(t, row[colHostApplication], "XD")
|
||||
assert.Equal(t, "UXP", row[colExtensionType])
|
||||
})
|
||||
|
||||
t.Run("missing manifest falls back to dir name", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
pluginPath := filepath.Join(dir, "some_uxp_ext")
|
||||
require.NoError(t, os.MkdirAll(pluginPath, 0o755))
|
||||
|
||||
sp := scanPath{extensionType: "UXP", user: "alice"}
|
||||
row := tbl.parseUXPPlugin(pluginPath, sp)
|
||||
require.NotNil(t, row)
|
||||
|
||||
assert.Equal(t, "some_uxp_ext", row[colName])
|
||||
assert.Equal(t, "alice", row[colUser])
|
||||
assert.Empty(t, row[colVersion])
|
||||
})
|
||||
|
||||
t.Run("manifest with empty name and id falls back to dir name", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
pluginPath := filepath.Join(dir, "dirname-fallback")
|
||||
require.NoError(t, os.MkdirAll(pluginPath, 0o755))
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(pluginPath, "manifest.json"),
|
||||
[]byte(`{"version": "1.0"}`),
|
||||
0o644,
|
||||
))
|
||||
|
||||
sp := scanPath{extensionType: "UXP"}
|
||||
row := tbl.parseUXPPlugin(pluginPath, sp)
|
||||
require.NotNil(t, row)
|
||||
|
||||
assert.Equal(t, "dirname-fallback", row[colName])
|
||||
assert.Equal(t, "1.0", row[colVersion])
|
||||
assert.Empty(t, row[colBundleID])
|
||||
})
|
||||
|
||||
t.Run("manifest with id but no name uses id", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
pluginPath := filepath.Join(dir, "id_only")
|
||||
require.NoError(t, os.MkdirAll(pluginPath, 0o755))
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(pluginPath, "manifest.json"),
|
||||
[]byte(`{"id": "com.vendor.idonly", "version": "1.0"}`),
|
||||
0o644,
|
||||
))
|
||||
|
||||
sp := scanPath{extensionType: "UXP"}
|
||||
row := tbl.parseUXPPlugin(pluginPath, sp)
|
||||
require.NotNil(t, row)
|
||||
|
||||
assert.Equal(t, "com.vendor.idonly", row[colName])
|
||||
assert.Equal(t, "com.vendor.idonly", row[colBundleID])
|
||||
assert.Equal(t, "1.0", row[colVersion])
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseNativePlugin(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fileName string
|
||||
expectedName string
|
||||
}{
|
||||
{"macOS plugin bundle", "MyPlugin.plugin", "MyPlugin"},
|
||||
{"Photoshop filter 8bf", "CoolFilter.8bf", "CoolFilter"},
|
||||
{"After Effects plugin", "Effect.aex", "Effect"},
|
||||
{"Windows DLL plugin", "Plugin.dll", "Plugin"},
|
||||
{"no extension", "SomePlugin", "SomePlugin"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
pluginPath := filepath.Join(dir, tt.fileName)
|
||||
|
||||
f, err := os.Create(pluginPath)
|
||||
require.NoError(t, err)
|
||||
f.Close()
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, entries, 1)
|
||||
|
||||
sp := scanPath{extensionType: "native", hostApp: "Photoshop"}
|
||||
row := parseNativePlugin(pluginPath, entries[0], sp)
|
||||
require.NotNil(t, row)
|
||||
|
||||
assert.Equal(t, tt.expectedName, row[colName])
|
||||
assert.Equal(t, "Photoshop", row[colHostApplication])
|
||||
assert.Equal(t, "native", row[colExtensionType])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveHostApps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
codes []string
|
||||
expected string
|
||||
}{
|
||||
{"single known code", []string{"PHXS"}, "Photoshop"},
|
||||
{"multiple codes", []string{"PHXS", "ILST"}, "Photoshop, Illustrator"},
|
||||
{"deduplicates same app", []string{"PHXS", "PHSP"}, "Photoshop"},
|
||||
{"unknown code passes through", []string{"UNKNOWN"}, "UNKNOWN"},
|
||||
{"mixed known and unknown", []string{"PPRO", "CUSTOM"}, "Premiere Pro, CUSTOM"},
|
||||
{"empty list", nil, ""},
|
||||
{"case insensitive", []string{"phxs"}, "Photoshop"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := resolveHostApps(tt.codes)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tbl := &adobePluginsTable{logger: zerolog.Nop()}
|
||||
|
||||
t.Run("CEP skips non-directory entries", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
f, err := os.Create(filepath.Join(dir, "notadir.txt"))
|
||||
require.NoError(t, err)
|
||||
f.Close()
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
sp := scanPath{extensionType: "CEP"}
|
||||
row := tbl.scanEntry(filepath.Join(dir, "notadir.txt"), entries[0], sp)
|
||||
assert.Nil(t, row)
|
||||
})
|
||||
|
||||
t.Run("native skips hidden files", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
f, err := os.Create(filepath.Join(dir, ".DS_Store"))
|
||||
require.NoError(t, err)
|
||||
f.Close()
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
sp := scanPath{extensionType: "native", hostApp: "Photoshop"}
|
||||
row := tbl.scanEntry(filepath.Join(dir, ".DS_Store"), entries[0], sp)
|
||||
assert.Nil(t, row)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOversizedManifestFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tbl := &adobePluginsTable{logger: zerolog.Nop()}
|
||||
|
||||
t.Run("CEP manifest over 1MB falls back to dir name", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
pluginPath := filepath.Join(dir, "huge-manifest")
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(pluginPath, "CSXS"), 0o755))
|
||||
|
||||
// Write a manifest larger than maxManifestSize (1MB)
|
||||
bigData := make([]byte, maxManifestSize+100)
|
||||
copy(bigData, []byte("<ExtensionManifest>"))
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(pluginPath, "CSXS", "manifest.xml"),
|
||||
bigData,
|
||||
0o644,
|
||||
))
|
||||
|
||||
sp := scanPath{extensionType: "CEP"}
|
||||
row := tbl.parseCEPPlugin(pluginPath, sp)
|
||||
require.NotNil(t, row)
|
||||
|
||||
assert.Equal(t, "huge-manifest", row[colName])
|
||||
assert.Empty(t, row[colVersion])
|
||||
assert.Equal(t, "CEP", row[colExtensionType])
|
||||
})
|
||||
|
||||
t.Run("UXP manifest over 1MB falls back to dir name", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
pluginPath := filepath.Join(dir, "huge-uxp")
|
||||
require.NoError(t, os.MkdirAll(pluginPath, 0o755))
|
||||
|
||||
bigData := make([]byte, maxManifestSize+100)
|
||||
copy(bigData, []byte(`{"name": "Should Not Parse"`))
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(pluginPath, "manifest.json"),
|
||||
bigData,
|
||||
0o644,
|
||||
))
|
||||
|
||||
sp := scanPath{extensionType: "UXP"}
|
||||
row := tbl.parseUXPPlugin(pluginPath, sp)
|
||||
require.NotNil(t, row)
|
||||
|
||||
assert.Equal(t, "huge-uxp", row[colName])
|
||||
assert.Empty(t, row[colVersion])
|
||||
assert.Equal(t, "UXP", row[colExtensionType])
|
||||
})
|
||||
}
|
||||
|
||||
func TestSymlinkSkipped(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tbl := &adobePluginsTable{logger: zerolog.Nop()}
|
||||
|
||||
dir := t.TempDir()
|
||||
// Create a real directory and a symlink to it
|
||||
realDir := filepath.Join(dir, "real-plugin")
|
||||
require.NoError(t, os.MkdirAll(realDir, 0o755))
|
||||
symlinkPath := filepath.Join(dir, "symlink-plugin")
|
||||
require.NoError(t, os.Symlink(realDir, symlinkPath))
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
sp := scanPath{extensionType: "CEP"}
|
||||
var skipped, kept int
|
||||
for _, entry := range entries {
|
||||
row := tbl.scanEntry(filepath.Join(dir, entry.Name()), entry, sp)
|
||||
if row == nil {
|
||||
skipped++
|
||||
} else {
|
||||
kept++
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, skipped, "symlink should be skipped")
|
||||
assert.Equal(t, 1, kept, "real directory should produce a row")
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//go:build windows
|
||||
|
||||
package adobe_plugins
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func getScanPaths(level string, logger zerolog.Logger) ([]scanPath, error) {
|
||||
var paths []scanPath
|
||||
|
||||
// System-wide CEP extensions
|
||||
paths = append(paths, scanPath{
|
||||
basePath: `C:\Program Files\Common Files\Adobe\CEP\extensions`,
|
||||
extensionType: "CEP",
|
||||
})
|
||||
paths = append(paths, scanPath{
|
||||
basePath: `C:\Program Files (x86)\Common Files\Adobe\CEP\extensions`,
|
||||
extensionType: "CEP",
|
||||
})
|
||||
|
||||
// System-wide UXP extensions
|
||||
paths = append(paths, scanPath{
|
||||
basePath: `C:\Program Files\Common Files\Adobe\UXP\extensions`,
|
||||
extensionType: "UXP",
|
||||
})
|
||||
|
||||
// Per-user CEP and UXP extensions
|
||||
users, err := listLocalUsers()
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to enumerate local users, skipping per-user paths")
|
||||
}
|
||||
for _, u := range users {
|
||||
paths = append(paths, scanPath{
|
||||
basePath: filepath.Join(u.homeDir, "AppData", "Roaming", "Adobe", "CEP", "extensions"),
|
||||
extensionType: "CEP",
|
||||
user: u.name,
|
||||
})
|
||||
paths = append(paths, scanPath{
|
||||
basePath: filepath.Join(u.homeDir, "AppData", "Roaming", "Adobe", "UXP", "extensions"),
|
||||
extensionType: "UXP",
|
||||
user: u.name,
|
||||
})
|
||||
}
|
||||
|
||||
if level == "deep" {
|
||||
paths = append(paths,
|
||||
scanPath{
|
||||
basePath: `C:\Program Files\Adobe\Adobe Photoshop *\Plug-ins`,
|
||||
extensionType: "native",
|
||||
hostApp: "Photoshop",
|
||||
},
|
||||
scanPath{
|
||||
basePath: `C:\Program Files\Adobe\Adobe Premiere Pro *\Plug-ins`,
|
||||
extensionType: "native",
|
||||
hostApp: "Premiere Pro",
|
||||
},
|
||||
scanPath{
|
||||
basePath: `C:\Program Files\Adobe\Adobe After Effects *\Plug-ins`,
|
||||
extensionType: "native",
|
||||
hostApp: "After Effects",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
type localUser struct {
|
||||
name string
|
||||
homeDir string
|
||||
}
|
||||
|
||||
func listLocalUsers() ([]localUser, error) {
|
||||
entries, err := os.ReadDir(`C:\Users`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
skipNames := map[string]struct{}{
|
||||
"public": {},
|
||||
"default": {},
|
||||
"default user": {},
|
||||
"all users": {},
|
||||
}
|
||||
|
||||
var users []localUser
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
if strings.HasPrefix(name, ".") {
|
||||
continue
|
||||
}
|
||||
if _, skip := skipNames[strings.ToLower(name)]; skip {
|
||||
continue
|
||||
}
|
||||
users = append(users, localUser{
|
||||
name: name,
|
||||
homeDir: filepath.Join(`C:\Users`, name),
|
||||
})
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ExtensionManifest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
ExtensionBundleId="com.example.test.plugin"
|
||||
ExtensionBundleVersion="2.1.0"
|
||||
Version="7.0">
|
||||
<Author Name="Test Vendor"/>
|
||||
<ExtensionList>
|
||||
<Extension Id="com.example.test.plugin.panel" Version="2.1.0"/>
|
||||
</ExtensionList>
|
||||
<ExecutionEnvironment>
|
||||
<HostList>
|
||||
<Host Name="PHXS" Version="[15.0,99.9]"/>
|
||||
<Host Name="ILST" Version="[19.0,99.9]"/>
|
||||
</HostList>
|
||||
</ExecutionEnvironment>
|
||||
</ExtensionManifest>
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "com.example.uxp.plugin",
|
||||
"name": "Test UXP Plugin",
|
||||
"version": "3.0.1",
|
||||
"host": [
|
||||
{"app": "PS", "minVersion": "22.0.0"},
|
||||
{"app": "XD", "minVersion": "36.0.0"}
|
||||
],
|
||||
"metadata": {
|
||||
"publisher": "UXP Test Vendor"
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ package table
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/table/adobe_plugins"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/table/app_sso_platform"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/table/authdb"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/table/codesign"
|
||||
@@ -52,6 +53,7 @@ import (
|
||||
func PlatformTables(opts PluginOpts) ([]osquery.OsqueryPlugin, error) {
|
||||
plugins := []osquery.OsqueryPlugin{
|
||||
// Fleet tables
|
||||
adobe_plugins.TablePlugin(log.Logger),
|
||||
table.NewPlugin("icloud_private_relay", privaterelay.Columns(), privaterelay.Generate),
|
||||
table.NewPlugin("user_login_settings", user_login_settings.Columns(), user_login_settings.Generate),
|
||||
table.NewPlugin("pwd_policy", pwd_policy.Columns(), pwd_policy.Generate),
|
||||
|
||||
@@ -5,6 +5,7 @@ package table
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/table/adobe_plugins"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/table/bitlocker_key_protectors"
|
||||
cisaudit "github.com/fleetdm/fleet/v4/orbit/pkg/table/cis_audit"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/table/mdm_bridge"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
func PlatformTables(_ PluginOpts) ([]osquery.OsqueryPlugin, error) {
|
||||
plugins := []osquery.OsqueryPlugin{
|
||||
// Fleet tables
|
||||
adobe_plugins.TablePlugin(log.Logger),
|
||||
table.NewPlugin("cis_audit", cisaudit.Columns(), cisaudit.Generate),
|
||||
|
||||
bitlocker_key_protectors.TablePlugin(log.Logger),
|
||||
|
||||
@@ -153,6 +153,81 @@
|
||||
],
|
||||
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/ad_config.yml"
|
||||
},
|
||||
{
|
||||
"name": "adobe_plugins",
|
||||
"platforms": [
|
||||
"darwin",
|
||||
"windows"
|
||||
],
|
||||
"description": "Detects Adobe plugins (CEP extensions, UXP extensions, and native plug-ins) installed on the host by scanning well-known directories and parsing plugin manifests.",
|
||||
"examples": "List all detected Adobe plugins (CEP and UXP extensions):\n\n```\nSELECT name, version, vendor, host_application, extension_type FROM adobe_plugins;\n```\n\nInclude native plug-ins from application directories (Photoshop, Premiere, etc.):\n\n```\nSELECT * FROM adobe_plugins WHERE scan_level = 'deep';\n```",
|
||||
"columns": [
|
||||
{
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"description": "Full filesystem path to the plugin directory or file."
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"description": "Plugin display name. From UXP manifest `name`, else manifest `id`, else directory or file name."
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"description": "Plugin version from the manifest. Empty if no manifest is found."
|
||||
},
|
||||
{
|
||||
"name": "vendor",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"description": "Plugin author or publisher from the manifest. Empty if absent."
|
||||
},
|
||||
{
|
||||
"name": "bundle_id",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"description": "Plugin bundle identifier from CEP `ExtensionBundleId` or UXP `id`. Empty for native plug-ins."
|
||||
},
|
||||
{
|
||||
"name": "host_application",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"description": "Target Adobe application(s) such as Photoshop, Illustrator, or Premiere Pro. Comma-separated if multiple."
|
||||
},
|
||||
{
|
||||
"name": "extension_type",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"description": "One of `CEP`, `UXP`, or `native`. Determined by the scan path, not by manifest presence."
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"description": "Local username for user-scoped plugin installs. Empty for system-wide installs."
|
||||
},
|
||||
{
|
||||
"name": "platform",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"description": "The host platform, either `darwin` or `windows`."
|
||||
},
|
||||
{
|
||||
"name": "scan_level",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"description": "WHERE-clause constraint that controls scan depth. `standard` (default) scans CEP and UXP directories only. `deep` additionally scans application-specific native plug-in directories."
|
||||
}
|
||||
],
|
||||
"notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
|
||||
"evented": false,
|
||||
"url": "https://fleetdm.com/tables/adobe_plugins",
|
||||
"fleetRepoUrl": "https://github.com/fleetdm/fleet/blob/main/schema/tables/adobe_plugins.yml"
|
||||
},
|
||||
{
|
||||
"name": "alf",
|
||||
"description": "Details about the status of the built-in firewall protection on this Mac.",
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
name: adobe_plugins
|
||||
platforms:
|
||||
- darwin
|
||||
- windows
|
||||
description: Detects Adobe plugins (CEP extensions, UXP extensions, and native plug-ins) installed on the host by scanning well-known directories and parsing plugin manifests.
|
||||
examples: |-
|
||||
List all detected Adobe plugins (CEP and UXP extensions):
|
||||
|
||||
```
|
||||
SELECT name, version, vendor, host_application, extension_type FROM adobe_plugins;
|
||||
```
|
||||
|
||||
Include native plug-ins from application directories (Photoshop, Premiere, etc.):
|
||||
|
||||
```
|
||||
SELECT * FROM adobe_plugins WHERE scan_level = 'deep';
|
||||
```
|
||||
columns:
|
||||
- name: path
|
||||
type: text
|
||||
required: false
|
||||
description: Full filesystem path to the plugin directory or file.
|
||||
- name: name
|
||||
type: text
|
||||
required: false
|
||||
description: Plugin display name. From UXP manifest `name`, else manifest `id`, else directory or file name.
|
||||
- name: version
|
||||
type: text
|
||||
required: false
|
||||
description: Plugin version from the manifest. Empty if no manifest is found.
|
||||
- name: vendor
|
||||
type: text
|
||||
required: false
|
||||
description: Plugin author or publisher from the manifest. Empty if absent.
|
||||
- name: bundle_id
|
||||
type: text
|
||||
required: false
|
||||
description: Plugin bundle identifier from CEP `ExtensionBundleId` or UXP `id`. Empty for native plug-ins.
|
||||
- name: host_application
|
||||
type: text
|
||||
required: false
|
||||
description: Target Adobe application(s) such as Photoshop, Illustrator, or Premiere Pro. Comma-separated if multiple.
|
||||
- name: extension_type
|
||||
type: text
|
||||
required: false
|
||||
description: One of `CEP`, `UXP`, or `native`. Determined by the scan path, not by manifest presence.
|
||||
- name: user
|
||||
type: text
|
||||
required: false
|
||||
description: Local username for user-scoped plugin installs. Empty for system-wide installs.
|
||||
- name: platform
|
||||
type: text
|
||||
required: false
|
||||
description: The host platform, either `darwin` or `windows`.
|
||||
- name: scan_level
|
||||
type: text
|
||||
required: false
|
||||
description: WHERE-clause constraint that controls scan depth. `standard` (default) scans CEP and UXP directories only. `deep` additionally scans application-specific native plug-in directories.
|
||||
notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
|
||||
evented: false
|
||||
Reference in New Issue
Block a user