Update fleetctl get software to list titles and versions. (#15444)

This commit is contained in:
Martin Angers 2023-12-06 16:07:03 -05:00 committed by GitHub
parent 6b128dd455
commit e3d225ade7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 281 additions and 58 deletions

View File

@ -0,0 +1 @@
* Updated `fleetctl get software` to list software titles, and add optional `--versions` flag to list software versions.

View File

@ -1213,12 +1213,16 @@ func getSoftwareCommand() *cli.Command {
return &cli.Command{
Name: "software",
Aliases: []string{"s"},
Usage: "List software",
Usage: "List software titles",
Flags: []cli.Flag{
&cli.UintFlag{
Name: teamFlagName,
Usage: "Only list software of hosts that belong to the specified team",
},
&cli.BoolFlag{
Name: "versions",
Usage: "List all software versions",
},
jsonFlag(),
yamlFlag(),
configFlag(),
@ -1242,13 +1246,22 @@ func getSoftwareCommand() *cli.Command {
query.Set("team_id", strconv.FormatUint(uint64(teamID), 10))
}
software, err := client.ListSoftware(query.Encode())
if c.Bool("versions") {
return printSoftwareVersions(c, client, query)
}
return printSoftwareTitles(c, client, query)
},
}
}
func printSoftwareVersions(c *cli.Context, client *service.Client, query url.Values) error {
software, err := client.ListSoftwareVersions(query.Encode())
if err != nil {
return fmt.Errorf("could not list software: %w", err)
return fmt.Errorf("could not list software versions: %w", err)
}
if len(software) == 0 {
log(c, "No software found")
log(c, "No software versions found")
return nil
}
@ -1273,16 +1286,62 @@ func getSoftwareCommand() *cli.Command {
s.Name,
s.Version,
s.Source,
s.GenerateCPE,
fmt.Sprint(len(s.Vulnerabilities)),
fmt.Sprintf("%d vulnerabilities", len(s.Vulnerabilities)),
fmt.Sprint(s.HostsCount),
})
}
columns := []string{"Name", "Version", "Source", "CPE", "# of CVEs"}
columns := []string{"Name", "Version", "Type", "Vulnerabilities", "Hosts"}
printTable(c, columns, data)
return nil
},
}
func printSoftwareTitles(c *cli.Context, client *service.Client, query url.Values) error {
software, err := client.ListSoftwareTitles(query.Encode())
if err != nil {
return fmt.Errorf("could not list software titles: %w", err)
}
if len(software) == 0 {
log(c, "No software titles found")
return nil
}
if c.Bool(jsonFlagName) || c.Bool(yamlFlagName) {
spec := specGeneric{
Kind: "software_title",
Version: "1",
Spec: software,
}
err = printSpec(c, spec)
if err != nil {
return err
}
return nil
}
// Default to printing as table
data := [][]string{}
for _, s := range software {
vulns := make(map[string]bool)
for _, ver := range s.Versions {
if ver.Vulnerabilities != nil {
for _, vuln := range *ver.Vulnerabilities {
vulns[vuln] = true
}
}
}
data = append(data, []string{
s.Name,
fmt.Sprintf("%d versions", s.VersionsCount),
s.Source,
fmt.Sprintf("%d vulnerabilities", len(vulns)),
fmt.Sprint(s.HostsCount),
})
}
columns := []string{"Name", "Versions", "Type", "Vulnerabilities", "Hosts"}
printTable(c, columns, data)
return nil
}
func getMDMAppleCommand() *cli.Command {

View File

@ -602,7 +602,159 @@ func TestGetConfig(t *testing.T) {
})
}
func TestGetSoftware(t *testing.T) {
func TestGetSoftwareTitles(t *testing.T) {
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{
License: &fleet.LicenseInfo{
Tier: fleet.TierPremium,
Expiration: time.Now().Add(24 * time.Hour),
},
})
var gotTeamID *uint
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions) ([]fleet.SoftwareTitle, int, *fleet.PaginationMetadata, error) {
gotTeamID = opt.TeamID
return []fleet.SoftwareTitle{
{
Name: "foo",
Source: "chrome_extensions",
HostsCount: 2,
VersionsCount: 3,
Versions: []fleet.SoftwareVersion{
{
Version: "0.0.1",
Vulnerabilities: &fleet.SliceString{"cve-123-456-001", "cve-123-456-002"},
},
{
Version: "0.0.2",
Vulnerabilities: &fleet.SliceString{"cve-123-456-001"},
},
{
Version: "0.0.3",
Vulnerabilities: &fleet.SliceString{"cve-123-456-003"},
},
},
},
{
Name: "bar",
Source: "deb_packages",
HostsCount: 0,
VersionsCount: 1,
Versions: []fleet.SoftwareVersion{
{
Version: "0.0.3",
Vulnerabilities: nil,
},
},
},
}, 0, nil, nil
}
expected := `+------+------------+-------------------+-------------------+-------+
| NAME | VERSIONS | TYPE | VULNERABILITIES | HOSTS |
+------+------------+-------------------+-------------------+-------+
| foo | 3 versions | chrome_extensions | 3 vulnerabilities | 2 |
+------+------------+-------------------+-------------------+-------+
| bar | 1 versions | deb_packages | 0 vulnerabilities | 0 |
+------+------------+-------------------+-------------------+-------+
`
expectedYaml := `---
apiVersion: "1"
kind: software_title
spec:
- hosts_count: 2
id: 0
name: foo
source: chrome_extensions
versions:
- id: 0
version: 0.0.1
vulnerabilities:
- cve-123-456-001
- cve-123-456-002
- id: 0
version: 0.0.2
vulnerabilities:
- cve-123-456-001
- id: 0
version: 0.0.3
vulnerabilities:
- cve-123-456-003
versions_count: 3
- hosts_count: 0
id: 0
name: bar
source: deb_packages
versions:
- id: 0
version: 0.0.3
versions_count: 1
`
expectedJson := `
{
"kind": "software_title",
"apiVersion": "1",
"spec": [
{
"id": 0,
"name": "foo",
"source": "chrome_extensions",
"hosts_count": 2,
"versions_count": 3,
"versions": [
{
"id": 0,
"version": "0.0.1",
"vulnerabilities": [
"cve-123-456-001",
"cve-123-456-002"
]
},
{
"id": 0,
"version": "0.0.2",
"vulnerabilities": [
"cve-123-456-001"
]
},
{
"id": 0,
"version": "0.0.3",
"vulnerabilities": [
"cve-123-456-003"
]
}
]
},
{
"id": 0,
"name": "bar",
"source": "deb_packages",
"hosts_count": 0,
"versions_count": 1,
"versions": [
{
"id": 0,
"version": "0.0.3"
}
]
}
]
}
`
assert.Equal(t, expected, runAppForTest(t, []string{"get", "software"}))
assert.YAMLEq(t, expectedYaml, runAppForTest(t, []string{"get", "software", "--yaml"}))
assert.JSONEq(t, expectedJson, runAppForTest(t, []string{"get", "software", "--json"}))
runAppForTest(t, []string{"get", "software", "--json", "--team", "999"})
require.NotNil(t, gotTeamID)
assert.Equal(t, uint(999), *gotTeamID)
}
func TestGetSoftwareVersions(t *testing.T) {
_, ds := runServerWithMockedDS(t)
foo001 := fleet.Software{
@ -627,17 +779,17 @@ func TestGetSoftware(t *testing.T) {
return 4, nil
}
expected := `+------+---------+-------------------+--------------------------+-----------+
| NAME | VERSION | SOURCE | CPE | # OF CVES |
+------+---------+-------------------+--------------------------+-----------+
| foo | 0.0.1 | chrome_extensions | somecpe | 2 |
+------+---------+-------------------+--------------------------+-----------+
| foo | 0.0.2 | chrome_extensions | | 0 |
+------+---------+-------------------+--------------------------+-----------+
| foo | 0.0.3 | chrome_extensions | someothercpewithoutvulns | 0 |
+------+---------+-------------------+--------------------------+-----------+
| bar | 0.0.3 | deb_packages | | 0 |
+------+---------+-------------------+--------------------------+-----------+
expected := `+------+---------+-------------------+-------------------+-------+
| NAME | VERSION | TYPE | VULNERABILITIES | HOSTS |
+------+---------+-------------------+-------------------+-------+
| foo | 0.0.1 | chrome_extensions | 2 vulnerabilities | 0 |
+------+---------+-------------------+-------------------+-------+
| foo | 0.0.2 | chrome_extensions | 0 vulnerabilities | 0 |
+------+---------+-------------------+-------------------+-------+
| foo | 0.0.3 | chrome_extensions | 0 vulnerabilities | 0 |
+------+---------+-------------------+-------------------+-------+
| bar | 0.0.3 | deb_packages | 0 vulnerabilities | 0 |
+------+---------+-------------------+-------------------+-------+
`
expectedYaml := `---
@ -736,11 +888,11 @@ spec:
}
`
assert.Equal(t, expected, runAppForTest(t, []string{"get", "software"}))
assert.YAMLEq(t, expectedYaml, runAppForTest(t, []string{"get", "software", "--yaml"}))
assert.JSONEq(t, expectedJson, runAppForTest(t, []string{"get", "software", "--json"}))
assert.Equal(t, expected, runAppForTest(t, []string{"get", "software", "--versions"}))
assert.YAMLEq(t, expectedYaml, runAppForTest(t, []string{"get", "software", "--versions", "--yaml"}))
assert.JSONEq(t, expectedJson, runAppForTest(t, []string{"get", "software", "--versions", "--json"}))
runAppForTest(t, []string{"get", "software", "--json", "--team", "999"})
runAppForTest(t, []string{"get", "software", "--versions", "--json", "--team", "999"})
require.NotNil(t, gotTeamID)
assert.Equal(t, uint(999), *gotTeamID)
}

View File

@ -4,8 +4,8 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
)
// ListSoftware retrieves the software running across hosts.
func (c *Client) ListSoftware(query string) ([]fleet.Software, error) {
// ListSoftwareVersions retrieves the software versions installed on hosts.
func (c *Client) ListSoftwareVersions(query string) ([]fleet.Software, error) {
verb, path := "GET", "/api/latest/fleet/software/versions"
var responseBody listSoftwareVersionsResponse
err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query)
@ -14,3 +14,14 @@ func (c *Client) ListSoftware(query string) ([]fleet.Software, error) {
}
return responseBody.Software, nil
}
// ListSoftwareTitles retrieves the software titles installed on hosts.
func (c *Client) ListSoftwareTitles(query string) ([]fleet.SoftwareTitle, error) {
verb, path := "GET", "/api/latest/fleet/software/titles"
var responseBody listSoftwareTitlesResponse
err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query)
if err != nil {
return nil, err
}
return responseBody.SoftwareTitles, nil
}

View File

@ -310,7 +310,7 @@ func getSoftwareFromURL(url, apiToken string, debug bool) []fleet.Software {
}
apiClient.SetToken(apiToken)
software, err := apiClient.ListSoftware("")
software, err := apiClient.ListSoftwareVersions("")
if err != nil {
panic(err)
}