Skip to content

Commit 22bec63

Browse files
feat(linux): add support for snap and flatpak pkgs
Signed-off-by: Swarit Pandey <swarit@stepsecurity.io>
1 parent ce74969 commit 22bec63

4 files changed

Lines changed: 180 additions & 4 deletions

File tree

internal/detector/syspkg.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,113 @@ func parsePackageList(stdout string, parseLine func(string) (string, string, boo
132132
return packages
133133
}
134134

135+
// DetectAdditionalManagers returns snap and/or flatpak if installed.
136+
// These coexist with the system PM — a machine can have rpm + snap + flatpak.
137+
func (d *SystemPkgDetector) DetectAdditionalManagers(ctx context.Context) []model.PkgManager {
138+
if d.exec.GOOS() != model.PlatformLinux {
139+
return nil
140+
}
141+
142+
type additionalPM struct {
143+
name string
144+
binary string
145+
versionCmd []string
146+
}
147+
148+
candidates := []additionalPM{
149+
{"snap", "snap", []string{"version"}},
150+
{"flatpak", "flatpak", []string{"--version"}},
151+
}
152+
153+
var managers []model.PkgManager
154+
for _, pm := range candidates {
155+
path, err := d.exec.LookPath(pm.binary)
156+
if err != nil {
157+
continue
158+
}
159+
160+
version := "unknown"
161+
stdout, _, _, err := d.exec.RunWithTimeout(ctx, 10*time.Second, pm.binary, pm.versionCmd...)
162+
if err == nil {
163+
if line := strings.TrimSpace(strings.SplitN(stdout, "\n", 2)[0]); line != "" {
164+
version = line
165+
}
166+
}
167+
168+
managers = append(managers, model.PkgManager{
169+
Name: pm.name,
170+
Version: version,
171+
Path: path,
172+
})
173+
}
174+
175+
return managers
176+
}
177+
178+
// ListSnapPackages returns installed snap packages.
179+
func (d *SystemPkgDetector) ListSnapPackages(ctx context.Context) []model.SystemPackage {
180+
if _, err := d.exec.LookPath("snap"); err != nil {
181+
return nil
182+
}
183+
184+
stdout, _, _, err := d.exec.RunWithTimeout(ctx, 30*time.Second, "snap", "list")
185+
if err != nil {
186+
return nil
187+
}
188+
189+
// snap list output: "Name Version Rev Tracking Publisher Notes"
190+
// Skip the header line
191+
lines := strings.Split(strings.TrimSpace(stdout), "\n")
192+
if len(lines) < 2 {
193+
return nil
194+
}
195+
196+
var packages []model.SystemPackage
197+
for _, line := range lines[1:] {
198+
fields := strings.Fields(line)
199+
if len(fields) >= 2 {
200+
packages = append(packages, model.SystemPackage{Name: fields[0], Version: fields[1]})
201+
}
202+
}
203+
return packages
204+
}
205+
206+
// ListFlatpakPackages returns installed flatpak applications.
207+
func (d *SystemPkgDetector) ListFlatpakPackages(ctx context.Context) []model.SystemPackage {
208+
if _, err := d.exec.LookPath("flatpak"); err != nil {
209+
return nil
210+
}
211+
212+
stdout, _, _, err := d.exec.RunWithTimeout(ctx, 30*time.Second,
213+
"flatpak", "list", "--app", "--columns=application,version")
214+
if err != nil {
215+
return nil
216+
}
217+
218+
stdout = strings.TrimSpace(stdout)
219+
if stdout == "" {
220+
return nil
221+
}
222+
223+
var packages []model.SystemPackage
224+
for _, line := range strings.Split(stdout, "\n") {
225+
line = strings.TrimSpace(line)
226+
if line == "" {
227+
continue
228+
}
229+
// Format: "app.id\tversion" (tab-separated)
230+
parts := strings.SplitN(line, "\t", 2)
231+
version := "unknown"
232+
if len(parts) >= 2 && parts[1] != "" {
233+
version = parts[1]
234+
}
235+
if parts[0] != "" {
236+
packages = append(packages, model.SystemPackage{Name: parts[0], Version: version})
237+
}
238+
}
239+
return packages
240+
}
241+
135242
// parseSpaceSeparated handles "name version" format (rpm, dpkg, pacman).
136243
func parseSpaceSeparated(line string) (string, string, bool) {
137244
parts := strings.SplitN(line, " ", 2)

internal/model/model.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ type ScanResult struct {
2020
PythonPkgManagers []PkgManager `json:"python_package_managers"`
2121
PythonPackages []PythonPackage `json:"python_packages"`
2222
PythonProjects []ProjectInfo `json:"python_projects"`
23-
SystemPkgManager *PkgManager `json:"system_package_manager,omitempty"`
24-
SystemPackages []SystemPackage `json:"system_packages"`
25-
Summary Summary `json:"summary"`
23+
SystemPkgManager *PkgManager `json:"system_package_manager,omitempty"`
24+
SystemPackages []SystemPackage `json:"system_packages"`
25+
SnapPkgManager *PkgManager `json:"snap_package_manager,omitempty"`
26+
SnapPackages []SystemPackage `json:"snap_packages"`
27+
FlatpakPkgManager *PkgManager `json:"flatpak_package_manager,omitempty"`
28+
FlatpakPackages []SystemPackage `json:"flatpak_packages"`
29+
Summary Summary `json:"summary"`
2630
}
2731

2832
type Device struct {
@@ -95,6 +99,8 @@ type Summary struct {
9599
BrewCasksCount int `json:"brew_casks_count"`
96100
PythonProjectsCount int `json:"python_projects_count"`
97101
SystemPackagesCount int `json:"system_packages_count"`
102+
SnapPackagesCount int `json:"snap_packages_count"`
103+
FlatpakPackagesCount int `json:"flatpak_packages_count"`
98104
}
99105

100106
// NodeScanResult holds raw scan output for enterprise telemetry.

internal/output/pretty.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ func Pretty(w io.Writer, result *model.ScanResult, colorMode string) error {
6262
if result.SystemPkgManager != nil {
6363
fmt.Fprintf(w, " %-24s %s%d%s\n", "System Packages", c.green, result.Summary.SystemPackagesCount, c.reset)
6464
}
65+
if result.SnapPkgManager != nil {
66+
fmt.Fprintf(w, " %-24s %s%d%s\n", "Snap Packages", c.green, result.Summary.SnapPackagesCount, c.reset)
67+
}
68+
if result.FlatpakPkgManager != nil {
69+
fmt.Fprintf(w, " %-24s %s%d%s\n", "Flatpak Apps", c.green, result.Summary.FlatpakPackagesCount, c.reset)
70+
}
6571
fmt.Fprintln(w)
6672

6773
// AI AGENTS AND TOOLS
@@ -226,6 +232,36 @@ func Pretty(w io.Writer, result *model.ScanResult, colorMode string) error {
226232
fmt.Fprintln(w)
227233
}
228234

235+
// SNAP PACKAGES (Linux only)
236+
if result.SnapPkgManager != nil {
237+
fmt.Fprintf(w, " %s%sSNAP PACKAGES%s\n", c.purple, c.bold, c.reset)
238+
fmt.Fprintln(w)
239+
if len(result.SnapPackages) > 0 {
240+
printSectionHeader(w, c, "Installed Snaps", len(result.SnapPackages))
241+
for _, pkg := range result.SnapPackages {
242+
fmt.Fprintf(w, " %-36s %s%s%s\n", pkg.Name, c.dim, pkg.Version, c.reset)
243+
}
244+
} else {
245+
fmt.Fprintf(w, " %sNo snap packages found%s\n", c.dim, c.reset)
246+
}
247+
fmt.Fprintln(w)
248+
}
249+
250+
// FLATPAK APPS (Linux only)
251+
if result.FlatpakPkgManager != nil {
252+
fmt.Fprintf(w, " %s%sFLATPAK APPS%s\n", c.purple, c.bold, c.reset)
253+
fmt.Fprintln(w)
254+
if len(result.FlatpakPackages) > 0 {
255+
printSectionHeader(w, c, "Installed Apps", len(result.FlatpakPackages))
256+
for _, pkg := range result.FlatpakPackages {
257+
fmt.Fprintf(w, " %-36s %s%s%s\n", pkg.Name, c.dim, pkg.Version, c.reset)
258+
}
259+
} else {
260+
fmt.Fprintf(w, " %sNo flatpak apps found%s\n", c.dim, c.reset)
261+
}
262+
fmt.Fprintln(w)
263+
}
264+
229265
return nil
230266
}
231267

internal/scan/scanner.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,11 @@ func Run(exec executor.Executor, log *progress.Logger, cfg *cli.Config) error {
125125
log.StepSkip("disabled (use --enable-brew-scan to enable)")
126126
}
127127

128-
// System package manager (Linux only — rpm, dpkg, pacman, apk)
128+
// System package managers (Linux only — rpm/dpkg/pacman/apk + snap + flatpak)
129129
var systemPkgManager *model.PkgManager
130130
var systemPackages []model.SystemPackage
131+
var snapPkgManager, flatpakPkgManager *model.PkgManager
132+
var snapPackages, flatpakPackages []model.SystemPackage
131133

132134
if exec.GOOS() == model.PlatformLinux {
133135
log.StepStart("Detecting system packages")
@@ -137,6 +139,19 @@ func Run(exec executor.Executor, log *progress.Logger, cfg *cli.Config) error {
137139
if systemPkgManager != nil {
138140
systemPackages = sysPkgDetector.ListPackages(ctx)
139141
}
142+
143+
// Snap and flatpak coexist with the system PM
144+
for _, mgr := range sysPkgDetector.DetectAdditionalManagers(ctx) {
145+
mgr := mgr
146+
switch mgr.Name {
147+
case "snap":
148+
snapPkgManager = &mgr
149+
snapPackages = sysPkgDetector.ListSnapPackages(ctx)
150+
case "flatpak":
151+
flatpakPkgManager = &mgr
152+
flatpakPackages = sysPkgDetector.ListFlatpakPackages(ctx)
153+
}
154+
}
140155
log.StepDone(time.Since(start))
141156
}
142157

@@ -206,6 +221,12 @@ func Run(exec executor.Executor, log *progress.Logger, cfg *cli.Config) error {
206221
if systemPackages == nil {
207222
systemPackages = []model.SystemPackage{}
208223
}
224+
if snapPackages == nil {
225+
snapPackages = []model.SystemPackage{}
226+
}
227+
if flatpakPackages == nil {
228+
flatpakPackages = []model.SystemPackage{}
229+
}
209230

210231
// Build result
211232
now := time.Now()
@@ -230,6 +251,10 @@ func Run(exec executor.Executor, log *progress.Logger, cfg *cli.Config) error {
230251
PythonProjects: pythonProjects,
231252
SystemPkgManager: systemPkgManager,
232253
SystemPackages: systemPackages,
254+
SnapPkgManager: snapPkgManager,
255+
SnapPackages: snapPackages,
256+
FlatpakPkgManager: flatpakPkgManager,
257+
FlatpakPackages: flatpakPackages,
233258
Summary: model.Summary{
234259
AIAgentsAndToolsCount: len(aiTools),
235260
IDEInstallationsCount: len(ides),
@@ -240,6 +265,8 @@ func Run(exec executor.Executor, log *progress.Logger, cfg *cli.Config) error {
240265
BrewCasksCount: len(brewCasks),
241266
PythonProjectsCount: len(pythonProjects),
242267
SystemPackagesCount: len(systemPackages),
268+
SnapPackagesCount: len(snapPackages),
269+
FlatpakPackagesCount: len(flatpakPackages),
243270
},
244271
}
245272

0 commit comments

Comments
 (0)