mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 06:48:54 +00:00
Feature 6975: Populate vendor column in software inventory (#7297)
- Populate 'software.vendor' when ingesting software from Windows hosts. - Increate width of 'software.vendor'.
This commit is contained in:
parent
2103de275a
commit
4013cbbdfc
6 changed files with 206 additions and 16 deletions
|
|
@ -0,0 +1 @@
|
|||
* Populate the vendor column on software ingested from Windows systems.
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20220818101352, Down_20220818101352)
|
||||
}
|
||||
|
||||
func Up_20220818101352(tx *sql.Tx) error {
|
||||
logger.Info.Println("Increasing width of software.vendor...")
|
||||
|
||||
//-----------------
|
||||
// Add temp column.
|
||||
//-----------------
|
||||
if _, err := tx.Exec(
|
||||
`ALTER TABLE software ADD COLUMN vendor_wide varchar(114) NULL, ALGORITHM=INPLACE, LOCK=NONE`); err != nil {
|
||||
return errors.Wrapf(err, "creating temp column for vendor")
|
||||
}
|
||||
|
||||
//---------------------
|
||||
// Add uniq constraint
|
||||
//---------------------
|
||||
if _, err := tx.Exec(
|
||||
"ALTER TABLE software ADD constraint unq_name UNIQUE (name, version, source, `release`, vendor_wide, arch)"); err != nil {
|
||||
return errors.Wrapf(err, "adding new uniquess constraint")
|
||||
}
|
||||
|
||||
//------------------
|
||||
// Update in batches
|
||||
//------------------
|
||||
const updateStmt = `UPDATE software SET vendor_wide = vendor WHERE vendor_wide IS NULL LIMIT 500`
|
||||
for {
|
||||
res, err := tx.Exec(updateStmt)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "updating temp vendor column")
|
||||
}
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "updating temp vendor column")
|
||||
}
|
||||
if affected == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//----------------
|
||||
// Drop old index
|
||||
//----------------
|
||||
if _, err := tx.Exec(`ALTER TABLE software DROP KEY name`); err != nil {
|
||||
return errors.Wrapf(err, "dropping old index")
|
||||
}
|
||||
|
||||
//------------------
|
||||
// Rename old column
|
||||
//------------------
|
||||
if _, err := tx.Exec(`ALTER TABLE software CHANGE vendor vendor_old varchar(32) DEFAULT '' NOT NULL, ALGORITHM=INPLACE, LOCK=NONE`); err != nil {
|
||||
return errors.Wrapf(err, "dropping old column")
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// Rename column
|
||||
// ---------------
|
||||
if _, err := tx.Exec(
|
||||
`ALTER TABLE software CHANGE vendor_wide vendor varchar(114) DEFAULT '' NOT NULL, ALGORITHM=INPLACE, LOCK=NONE`); err != nil {
|
||||
return errors.Wrapf(err, "dropping old column")
|
||||
}
|
||||
|
||||
logger.Info.Println("Done increasing width of software.vendor...")
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20220818101352(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20220818101352(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
_, err := db.Exec(`INSERT INTO software (name, version, source, bundle_identifier, vendor, arch)
|
||||
VALUES
|
||||
('zchunk-libs', '1.2.1', 'rpm_packages', '', 'Fedora Project', 'x86_64'),
|
||||
('word', '1.2.1', 'rpm_packages', '', 'Fake MS', 'x86_64'),
|
||||
('excel', '1.2.1', 'rpm_packages', '', '', 'x86_64')
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
// Check all old vendors are still there
|
||||
var vendors []string
|
||||
err = db.Select(&vendors, `SELECT vendor FROM software`)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []string{"Fedora Project", "Fake MS", ""}, vendors)
|
||||
|
||||
// Check we can store a longer vendors
|
||||
randVendor := `
|
||||
oFZTwTV5WxJt02EVHEBcnhLzuJ8wnxKwfbabPWy7yTSiQbabEcAGDVmoXKZEZJLWObGD0cVfYptInHYgKjtDeDsBh2a8669EnyAqyBECXbFjSh`
|
||||
|
||||
_, err = db.Exec(
|
||||
`INSERT INTO software (name, version, source, bundle_identifier, vendor, arch) VALUES ('zchunk-libs', '1.2.1', 'rpm_packages', '', ?, 'x86_64')`,
|
||||
randVendor,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
@ -34,6 +34,10 @@ type CVEMeta struct {
|
|||
Published *time.Time `db:"published"`
|
||||
}
|
||||
|
||||
// Must be kept in sync with the vendor column definition.
|
||||
const SoftwareVendorMaxLength = 114
|
||||
const SoftwareVendorMaxLengthFmt = "%.111s..."
|
||||
|
||||
// Software is a named and versioned piece of software installed on a device.
|
||||
type Software struct {
|
||||
ID uint `json:"id" db:"id"`
|
||||
|
|
@ -51,6 +55,12 @@ type Software struct {
|
|||
Release string `json:"release,omitempty" db:"release"`
|
||||
// Vendor is the supplier of the software (e.g. "CentOS").
|
||||
Vendor string `json:"vendor,omitempty" db:"vendor"`
|
||||
|
||||
// TODO: Remove this as part of the clean up of https://github.com/fleetdm/fleet/pull/7297
|
||||
// DO NOT USE THIS, use 'Vendor' instead. We had to 'recreate' the vendor column because we
|
||||
// needed to make it wider - the old column was left and renamed to 'vendor_old'
|
||||
VendorOld string `json:"-" db:"vendor_old"`
|
||||
|
||||
// Arch is the architecture of the software (e.g. "x86_64").
|
||||
Arch string `json:"arch,omitempty" db:"arch"`
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
|
|
@ -581,57 +582,57 @@ SELECT
|
|||
name AS name,
|
||||
version AS version,
|
||||
'Program (Windows)' AS type,
|
||||
'programs' AS source
|
||||
'programs' AS source,
|
||||
publisher AS vendor
|
||||
FROM programs
|
||||
UNION
|
||||
SELECT
|
||||
name AS name,
|
||||
version AS version,
|
||||
'Package (Python)' AS type,
|
||||
'python_packages' AS source
|
||||
'python_packages' AS source,
|
||||
'' AS vendor
|
||||
FROM python_packages
|
||||
UNION
|
||||
SELECT
|
||||
name AS name,
|
||||
version AS version,
|
||||
'Browser plugin (IE)' AS type,
|
||||
'ie_extensions' AS source
|
||||
'ie_extensions' AS source,
|
||||
'' AS vendor
|
||||
FROM ie_extensions
|
||||
UNION
|
||||
SELECT
|
||||
name AS name,
|
||||
version AS version,
|
||||
'Browser plugin (Chrome)' AS type,
|
||||
'chrome_extensions' AS source
|
||||
'chrome_extensions' AS source,
|
||||
'' AS vendor
|
||||
FROM cached_users CROSS JOIN chrome_extensions USING (uid)
|
||||
UNION
|
||||
SELECT
|
||||
name AS name,
|
||||
version AS version,
|
||||
'Browser plugin (Firefox)' AS type,
|
||||
'firefox_addons' AS source
|
||||
'firefox_addons' AS source,
|
||||
'' AS vendor
|
||||
FROM cached_users CROSS JOIN firefox_addons USING (uid)
|
||||
UNION
|
||||
SELECT
|
||||
name AS name,
|
||||
version AS version,
|
||||
'Package (Chocolatey)' AS type,
|
||||
'chocolatey_packages' AS source
|
||||
'chocolatey_packages' AS source,
|
||||
'' AS vendor
|
||||
FROM chocolatey_packages
|
||||
UNION
|
||||
SELECT
|
||||
name AS name,
|
||||
version AS version,
|
||||
'Package (Atom)' AS type,
|
||||
'atom_packages' AS source
|
||||
FROM cached_users CROSS JOIN atom_packages USING (uid)
|
||||
UNION
|
||||
SELECT
|
||||
name AS name,
|
||||
version AS version,
|
||||
'Package (Python)' AS type,
|
||||
'python_packages' AS source
|
||||
FROM python_packages;
|
||||
'atom_packages' AS source,
|
||||
'' AS vendor
|
||||
FROM cached_users CROSS JOIN atom_packages USING (uid);
|
||||
`),
|
||||
Platforms: []string{"windows"},
|
||||
DirectIngestFunc: directIngestSoftware,
|
||||
|
|
@ -861,6 +862,8 @@ func directIngestSoftware(ctx context.Context, logger log.Logger, host *fleet.Ho
|
|||
version := row["version"]
|
||||
source := row["source"]
|
||||
bundleIdentifier := row["bundle_identifier"]
|
||||
vendor := row["vendor"]
|
||||
|
||||
if name == "" {
|
||||
level.Debug(logger).Log(
|
||||
"msg", "host reported software with empty name",
|
||||
|
|
@ -895,6 +898,11 @@ func directIngestSoftware(ctx context.Context, logger log.Logger, host *fleet.Ho
|
|||
}
|
||||
}
|
||||
|
||||
// Check whether the vendor is longer than the max allowed width and if so, truncate it.
|
||||
if utf8.RuneCountInString(vendor) >= fleet.SoftwareVendorMaxLength {
|
||||
vendor = fmt.Sprintf(fleet.SoftwareVendorMaxLengthFmt, vendor)
|
||||
}
|
||||
|
||||
s := fleet.Software{
|
||||
Name: name,
|
||||
Version: version,
|
||||
|
|
@ -902,7 +910,7 @@ func directIngestSoftware(ctx context.Context, logger log.Logger, host *fleet.Ho
|
|||
BundleIdentifier: bundleIdentifier,
|
||||
|
||||
Release: row["release"],
|
||||
Vendor: row["vendor"],
|
||||
Vendor: vendor,
|
||||
Arch: row["arch"],
|
||||
}
|
||||
if !lastOpenedAt.IsZero() {
|
||||
|
|
|
|||
|
|
@ -657,3 +657,58 @@ func TestDangerousReplaceQuery(t *testing.T) {
|
|||
queries = GetDetailQueries(&fleet.AppConfig{HostSettings: fleet.HostSettings{EnableHostUsers: true}}, config.FleetConfig{})
|
||||
assert.Equal(t, originalQuery, queries["users"].Query)
|
||||
}
|
||||
|
||||
func TestDirectIngestSoftware(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
|
||||
t.Run("vendor gets truncated", func(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
data []map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
data: []map[string]string{
|
||||
{
|
||||
"name": "Software 1",
|
||||
"version": "12.5",
|
||||
"source": "My backyard",
|
||||
"bundle_identifier": "",
|
||||
"vendor": "Fleet",
|
||||
},
|
||||
},
|
||||
expected: "Fleet",
|
||||
},
|
||||
{
|
||||
data: []map[string]string{
|
||||
{
|
||||
"name": "Software 1",
|
||||
"version": "12.5",
|
||||
"source": "My backyard",
|
||||
"bundle_identifier": "",
|
||||
"vendor": `oFZTwTV5WxJt02EVHEBcnhLzuJ8wnxKwfbabPWy7yTSiQbabEcAGDVmoXKZEZJLWObGD0cVfYptInHYgKjtDeDsBh2a8669EnyAqyBECXbFjSh1111`,
|
||||
},
|
||||
},
|
||||
expected: `oFZTwTV5WxJt02EVHEBcnhLzuJ8wnxKwfbabPWy7yTSiQbabEcAGDVmoXKZEZJLWObGD0cVfYptInHYgKjtDeDsBh2a8669EnyAqyBECXbFjSh1...`,
|
||||
},
|
||||
} {
|
||||
ds.UpdateHostSoftwareFunc = func(ctx context.Context, hostID uint, software []fleet.Software) error {
|
||||
require.Len(t, software, 1)
|
||||
require.Equal(t, tc.expected, software[0].Vendor)
|
||||
return nil
|
||||
}
|
||||
|
||||
err := directIngestSoftware(
|
||||
context.Background(),
|
||||
log.NewNopLogger(),
|
||||
&fleet.Host{ID: uint(i)},
|
||||
ds,
|
||||
tc.data,
|
||||
false,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.True(t, ds.UpdateHostSoftwareFuncInvoked)
|
||||
ds.UpdateHostSoftwareFuncInvoked = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue