2021-04-26 15:44:22 +00:00
|
|
|
package mysql
|
|
|
|
|
|
|
|
|
|
import (
|
2021-09-14 12:11:07 +00:00
|
|
|
"context"
|
2022-04-20 14:02:35 +00:00
|
|
|
"database/sql"
|
|
|
|
|
"errors"
|
2021-04-26 15:44:22 +00:00
|
|
|
"fmt"
|
Fix deadlock when deleting software during data ingestion (#15459)
This fixes the deadlock reported in #14779.
We found a deadlock in software ingestion during load tests performed in
October:
```
2023-10-26T17:20:41.719627Z 0 [Note] [MY-012468] [InnoDB] Transactions deadlock detected, dumping detailed information. (lock0lock.cc:6482)
2023-10-26T17:20:41.719661Z 0 [Note] [MY-012469] [InnoDB] *** (1) TRANSACTION: (lock0lock.cc:6496)
TRANSACTION 3069866646, ACTIVE 0 sec starting index read
mysql tables in use 2, locked 2
LOCK WAIT 8 lock struct(s), heap size 1136, 18 row lock(s), undo log entries 10
MySQL thread id 95, OS thread handle 70431326097136, query id 340045 10.12.3.105 fleet executing
DELETE FROM software WHERE id IN (165, 79, 344, 47, 212, 21, 60, 127, 173, 145) AND
NOT EXISTS (
SELECT 1 FROM host_software hsw WHERE hsw.software_id = software.id
)
2023-10-26T17:20:41.719700Z 0 [Note] [MY-012469] [InnoDB] *** (1) HOLDS THE LOCK(S): (lock0lock.cc:6496)
RECORD LOCKS space id 932 page no 8 n bits 256 index PRIMARY of table `fleet`.`software` trx id 3069866646 lock_mode X locks rec but not gap
Record lock, heap no 22 PHYSICAL RECORD: n_fields 11; compact format; info bits 0
0: len 8; hex 0000000000000015; asc ;;
1: len 6; hex 0000a74c4a7c; asc LJ|;;
2: len 7; hex 82000000d00264; asc d;;
3: len 26; hex 616e74692d76697275735f666f725f736f70686f735f686f6d65; asc anti-virus_for_sophos_home;;
4: len 5; hex 322e322e36; asc 2.2.6;;
5: len 4; hex 61707073; asc apps;;
6: len 0; hex ; asc ;;
7: len 0; hex ; asc ;;
8: len 0; hex ; asc ;;
9: len 0; hex ; asc ;;
10: len 0; hex ; asc ;;
Record lock, heap no 48 PHYSICAL RECORD: n_fields 11; compact format; info bits 0
0: len 8; hex 000000000000002f; asc /;;
1: len 6; hex 0000a74c4aad; asc LJ ;;
2: len 7; hex 81000000e30220; asc ;;
3: len 10; hex 7265616c706c61796572; asc realplayer;;
4: len 11; hex 31322e302e312e31373338; asc 12.0.1.1738;;
5: len 4; hex 61707073; asc apps;;
6: len 0; hex ; asc ;;
7: len 0; hex ; asc ;;
8: len 0; hex ; asc ;;
9: len 0; hex ; asc ;;
10: len 0; hex ; asc ;;
Record lock, heap no 61 PHYSICAL RECORD: n_fields 11; compact format; info bits 0
0: len 8; hex 000000000000003c; asc <;;
1: len 6; hex 0000a74c4afb; asc LJ ;;
2: len 7; hex 820000017501ba; asc u ;;
3: len 7; hex 636f6e6e656374; asc connect;;
4: len 5; hex 332e322e37; asc 3.2.7;;
5: len 4; hex 61707073; asc apps;;
6: len 0; hex ; asc ;;
7: len 0; hex ; asc ;;
8: len 0; hex ; asc ;;
9: len 0; hex ; asc ;;
10: len 0; hex ; asc ;;
Record lock, heap no 80 PHYSICAL RECORD: n_fields 11; compact format; info bits 0
0: len 8; hex 000000000000004f; asc O;;
1: len 6; hex 0000a74c4b32; asc LK2;;
2: len 7; hex 820000008a01cb; asc ;;
3: len 7; hex 68697063686174; asc hipchat;;
4: len 4; hex 342e3330; asc 4.30;;
5: len 4; hex 61707073; asc apps;;
6: len 0; hex ; asc ;;
7: len 0; hex ; asc ;;
8: len 0; hex ; asc ;;
9: len 0; hex ; asc ;;
10: len 0; hex ; asc ;;
2023-10-26T17:20:41.720564Z 0 [Note] [MY-012469] [InnoDB] *** (1) WAITING FOR THIS LOCK TO BE GRANTED: (lock0lock.cc:6496)
RECORD LOCKS space id 695 page no 5994 n bits 1000 index host_software_software_id_fk of table `fleet`.`host_software` trx id 3069866646 lock mode S waiting
Record lock, heap no 31 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 8; hex 000000000000004f; asc O;;
1: len 4; hex 0000000c; asc ;;
2023-10-26T17:20:41.720650Z 0 [Note] [MY-012469] [InnoDB] *** (2) TRANSACTION: (lock0lock.cc:6496)
TRANSACTION 3069866680, ACTIVE 0 sec starting index read
mysql tables in use 2, locked 2
LOCK WAIT 7 lock struct(s), heap size 1136, 12 row lock(s), undo log entries 8
MySQL thread id 98, OS thread handle 70375801900784, query id 340524 10.12.3.9 fleet executing
DELETE FROM software WHERE id IN (49, 113, 183, 187, 223, 79, 81, 116) AND
NOT EXISTS (
SELECT 1 FROM host_software hsw WHERE hsw.software_id = software.id
)
2023-10-26T17:20:41.720682Z 0 [Note] [MY-012469] [InnoDB] *** (2) HOLDS THE LOCK(S): (lock0lock.cc:6496)
RECORD LOCKS space id 695 page no 5994 n bits 1000 index host_software_software_id_fk of table `fleet`.`host_software` trx id 3069866680 lock_mode X locks rec but not gap
Record lock, heap no 31 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 8; hex 000000000000004f; asc O;;
1: len 4; hex 0000000c; asc ;;
2023-10-26T17:20:41.720760Z 0 [Note] [MY-012469] [InnoDB] *** (2) WAITING FOR THIS LOCK TO BE GRANTED: (lock0lock.cc:6496)
RECORD LOCKS space id 932 page no 8 n bits 256 index PRIMARY of table `fleet`.`software` trx id 3069866680 lock_mode X locks rec but not gap waiting
Record lock, heap no 80 PHYSICAL RECORD: n_fields 11; compact format; info bits 0
0: len 8; hex 000000000000004f; asc O;;
1: len 6; hex 0000a74c4b32; asc LK2;;
2: len 7; hex 820000008a01cb; asc ;;
3: len 7; hex 68697063686174; asc hipchat;;
4: len 4; hex 342e3330; asc 4.30;;
5: len 4; hex 61707073; asc apps;;
6: len 0; hex ; asc ;;
7: len 0; hex ; asc ;;
8: len 0; hex ; asc ;;
9: len 0; hex ; asc ;;
10: len 0; hex ; asc ;;
2023-10-26T17:20:41.720984Z 0 [Note] [MY-012469] [InnoDB] *** WE ROLL BACK TRANSACTION (2) (lock0lock.cc:6496)
```
I was able to reproduce this issue on `main` with the added test. The
solution is to remove the deletion (cleanup) of `software` to a separate
transaction after the main transaction is done.
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
2023-12-07 12:34:53 +00:00
|
|
|
"slices"
|
2021-11-08 16:07:42 +00:00
|
|
|
"sort"
|
2021-04-26 15:44:22 +00:00
|
|
|
"strings"
|
2022-01-26 14:47:56 +00:00
|
|
|
"time"
|
2021-04-26 15:44:22 +00:00
|
|
|
|
2021-11-04 18:21:39 +00:00
|
|
|
"github.com/doug-martin/goqu/v9"
|
|
|
|
|
_ "github.com/doug-martin/goqu/v9/dialect/mysql"
|
2021-11-15 14:11:38 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
2021-06-26 04:46:51 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
2023-12-04 16:09:23 +00:00
|
|
|
"github.com/go-kit/kit/log/level"
|
2021-04-26 15:44:22 +00:00
|
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
|
)
|
|
|
|
|
|
2024-05-08 14:27:17 +00:00
|
|
|
// Since DB may have millions of software items, we need to batch the aggregation counts to avoid long SQL query times.
|
|
|
|
|
// This is a variable so it can be adjusted during unit testing.
|
|
|
|
|
var countHostSoftwareBatchSize = uint64(100000)
|
|
|
|
|
|
2022-04-26 18:16:59 +00:00
|
|
|
func softwareSliceToMap(softwares []fleet.Software) map[string]fleet.Software {
|
|
|
|
|
result := make(map[string]fleet.Software)
|
2021-07-08 16:57:43 +00:00
|
|
|
for _, s := range softwares {
|
2023-05-17 18:49:09 +00:00
|
|
|
result[s.ToUniqueStr()] = s
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-17 18:49:09 +00:00
|
|
|
func (ds *Datastore) UpdateHostSoftware(ctx context.Context, hostID uint, software []fleet.Software) (*fleet.UpdateHostSoftwareDBResult, error) {
|
|
|
|
|
var result *fleet.UpdateHostSoftwareDBResult
|
|
|
|
|
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
|
|
|
|
r, err := applyChangesForNewSoftwareDB(ctx, tx, hostID, software, ds.minLastOpenedAtDiff)
|
|
|
|
|
result = r
|
|
|
|
|
return err
|
|
|
|
|
})
|
Fix deadlock when deleting software during data ingestion (#15459)
This fixes the deadlock reported in #14779.
We found a deadlock in software ingestion during load tests performed in
October:
```
2023-10-26T17:20:41.719627Z 0 [Note] [MY-012468] [InnoDB] Transactions deadlock detected, dumping detailed information. (lock0lock.cc:6482)
2023-10-26T17:20:41.719661Z 0 [Note] [MY-012469] [InnoDB] *** (1) TRANSACTION: (lock0lock.cc:6496)
TRANSACTION 3069866646, ACTIVE 0 sec starting index read
mysql tables in use 2, locked 2
LOCK WAIT 8 lock struct(s), heap size 1136, 18 row lock(s), undo log entries 10
MySQL thread id 95, OS thread handle 70431326097136, query id 340045 10.12.3.105 fleet executing
DELETE FROM software WHERE id IN (165, 79, 344, 47, 212, 21, 60, 127, 173, 145) AND
NOT EXISTS (
SELECT 1 FROM host_software hsw WHERE hsw.software_id = software.id
)
2023-10-26T17:20:41.719700Z 0 [Note] [MY-012469] [InnoDB] *** (1) HOLDS THE LOCK(S): (lock0lock.cc:6496)
RECORD LOCKS space id 932 page no 8 n bits 256 index PRIMARY of table `fleet`.`software` trx id 3069866646 lock_mode X locks rec but not gap
Record lock, heap no 22 PHYSICAL RECORD: n_fields 11; compact format; info bits 0
0: len 8; hex 0000000000000015; asc ;;
1: len 6; hex 0000a74c4a7c; asc LJ|;;
2: len 7; hex 82000000d00264; asc d;;
3: len 26; hex 616e74692d76697275735f666f725f736f70686f735f686f6d65; asc anti-virus_for_sophos_home;;
4: len 5; hex 322e322e36; asc 2.2.6;;
5: len 4; hex 61707073; asc apps;;
6: len 0; hex ; asc ;;
7: len 0; hex ; asc ;;
8: len 0; hex ; asc ;;
9: len 0; hex ; asc ;;
10: len 0; hex ; asc ;;
Record lock, heap no 48 PHYSICAL RECORD: n_fields 11; compact format; info bits 0
0: len 8; hex 000000000000002f; asc /;;
1: len 6; hex 0000a74c4aad; asc LJ ;;
2: len 7; hex 81000000e30220; asc ;;
3: len 10; hex 7265616c706c61796572; asc realplayer;;
4: len 11; hex 31322e302e312e31373338; asc 12.0.1.1738;;
5: len 4; hex 61707073; asc apps;;
6: len 0; hex ; asc ;;
7: len 0; hex ; asc ;;
8: len 0; hex ; asc ;;
9: len 0; hex ; asc ;;
10: len 0; hex ; asc ;;
Record lock, heap no 61 PHYSICAL RECORD: n_fields 11; compact format; info bits 0
0: len 8; hex 000000000000003c; asc <;;
1: len 6; hex 0000a74c4afb; asc LJ ;;
2: len 7; hex 820000017501ba; asc u ;;
3: len 7; hex 636f6e6e656374; asc connect;;
4: len 5; hex 332e322e37; asc 3.2.7;;
5: len 4; hex 61707073; asc apps;;
6: len 0; hex ; asc ;;
7: len 0; hex ; asc ;;
8: len 0; hex ; asc ;;
9: len 0; hex ; asc ;;
10: len 0; hex ; asc ;;
Record lock, heap no 80 PHYSICAL RECORD: n_fields 11; compact format; info bits 0
0: len 8; hex 000000000000004f; asc O;;
1: len 6; hex 0000a74c4b32; asc LK2;;
2: len 7; hex 820000008a01cb; asc ;;
3: len 7; hex 68697063686174; asc hipchat;;
4: len 4; hex 342e3330; asc 4.30;;
5: len 4; hex 61707073; asc apps;;
6: len 0; hex ; asc ;;
7: len 0; hex ; asc ;;
8: len 0; hex ; asc ;;
9: len 0; hex ; asc ;;
10: len 0; hex ; asc ;;
2023-10-26T17:20:41.720564Z 0 [Note] [MY-012469] [InnoDB] *** (1) WAITING FOR THIS LOCK TO BE GRANTED: (lock0lock.cc:6496)
RECORD LOCKS space id 695 page no 5994 n bits 1000 index host_software_software_id_fk of table `fleet`.`host_software` trx id 3069866646 lock mode S waiting
Record lock, heap no 31 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 8; hex 000000000000004f; asc O;;
1: len 4; hex 0000000c; asc ;;
2023-10-26T17:20:41.720650Z 0 [Note] [MY-012469] [InnoDB] *** (2) TRANSACTION: (lock0lock.cc:6496)
TRANSACTION 3069866680, ACTIVE 0 sec starting index read
mysql tables in use 2, locked 2
LOCK WAIT 7 lock struct(s), heap size 1136, 12 row lock(s), undo log entries 8
MySQL thread id 98, OS thread handle 70375801900784, query id 340524 10.12.3.9 fleet executing
DELETE FROM software WHERE id IN (49, 113, 183, 187, 223, 79, 81, 116) AND
NOT EXISTS (
SELECT 1 FROM host_software hsw WHERE hsw.software_id = software.id
)
2023-10-26T17:20:41.720682Z 0 [Note] [MY-012469] [InnoDB] *** (2) HOLDS THE LOCK(S): (lock0lock.cc:6496)
RECORD LOCKS space id 695 page no 5994 n bits 1000 index host_software_software_id_fk of table `fleet`.`host_software` trx id 3069866680 lock_mode X locks rec but not gap
Record lock, heap no 31 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 8; hex 000000000000004f; asc O;;
1: len 4; hex 0000000c; asc ;;
2023-10-26T17:20:41.720760Z 0 [Note] [MY-012469] [InnoDB] *** (2) WAITING FOR THIS LOCK TO BE GRANTED: (lock0lock.cc:6496)
RECORD LOCKS space id 932 page no 8 n bits 256 index PRIMARY of table `fleet`.`software` trx id 3069866680 lock_mode X locks rec but not gap waiting
Record lock, heap no 80 PHYSICAL RECORD: n_fields 11; compact format; info bits 0
0: len 8; hex 000000000000004f; asc O;;
1: len 6; hex 0000a74c4b32; asc LK2;;
2: len 7; hex 820000008a01cb; asc ;;
3: len 7; hex 68697063686174; asc hipchat;;
4: len 4; hex 342e3330; asc 4.30;;
5: len 4; hex 61707073; asc apps;;
6: len 0; hex ; asc ;;
7: len 0; hex ; asc ;;
8: len 0; hex ; asc ;;
9: len 0; hex ; asc ;;
10: len 0; hex ; asc ;;
2023-10-26T17:20:41.720984Z 0 [Note] [MY-012469] [InnoDB] *** WE ROLL BACK TRANSACTION (2) (lock0lock.cc:6496)
```
I was able to reproduce this issue on `main` with the added test. The
solution is to remove the deletion (cleanup) of `software` to a separate
transaction after the main transaction is done.
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
2023-12-07 12:34:53 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return result, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We perform the following cleanup on a separate transaction to avoid deadlocks.
|
|
|
|
|
//
|
|
|
|
|
// Cleanup the software table when no more hosts have the deleted host_software
|
|
|
|
|
// table entries. Otherwise the software will be listed by ds.ListSoftware but
|
|
|
|
|
// ds.SoftwareByID, ds.CountHosts and ds.ListHosts will return a *notFoundError
|
|
|
|
|
// error for such software.
|
|
|
|
|
if len(result.Deleted) > 0 {
|
|
|
|
|
deletesHostSoftwareIDs := make([]uint, 0, len(result.Deleted))
|
|
|
|
|
for _, software := range result.Deleted {
|
|
|
|
|
deletesHostSoftwareIDs = append(deletesHostSoftwareIDs, software.ID)
|
|
|
|
|
}
|
|
|
|
|
slices.Sort(deletesHostSoftwareIDs)
|
|
|
|
|
if err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
|
|
|
|
stmt := `DELETE FROM software WHERE id IN (?) AND NOT EXISTS (
|
|
|
|
|
SELECT 1 FROM host_software hsw WHERE hsw.software_id = software.id
|
|
|
|
|
)`
|
|
|
|
|
stmt, args, err := sqlx.In(stmt, deletesHostSoftwareIDs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ctxerr.Wrap(ctx, err, "build delete software query")
|
|
|
|
|
}
|
|
|
|
|
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
|
|
|
|
|
return ctxerr.Wrap(ctx, err, "delete software")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}); err != nil {
|
|
|
|
|
return result, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-17 18:49:09 +00:00
|
|
|
return result, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ds *Datastore) UpdateHostSoftwareInstalledPaths(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
hostID uint,
|
|
|
|
|
reported map[string]struct{},
|
|
|
|
|
mutationResults *fleet.UpdateHostSoftwareDBResult,
|
|
|
|
|
) error {
|
|
|
|
|
currS := mutationResults.CurrInstalled()
|
|
|
|
|
|
|
|
|
|
hsip, err := ds.getHostSoftwareInstalledPaths(ctx, hostID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toI, toD, err := hostSoftwareInstalledPathsDelta(hostID, reported, hsip, currS)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(toI) == 0 && len(toD) == 0 {
|
|
|
|
|
// Nothing to do ...
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-03 17:56:22 +00:00
|
|
|
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
2023-05-17 18:49:09 +00:00
|
|
|
if err := deleteHostSoftwareInstalledPaths(ctx, tx, toD); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := insertHostSoftwareInstalledPaths(ctx, tx, toI); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
2021-09-08 18:43:22 +00:00
|
|
|
})
|
|
|
|
|
}
|
2021-04-26 15:44:22 +00:00
|
|
|
|
2023-05-17 20:53:15 +00:00
|
|
|
// getHostSoftwareInstalledPaths returns all HostSoftwareInstalledPath for the given hostID.
|
2023-05-17 18:49:09 +00:00
|
|
|
func (ds *Datastore) getHostSoftwareInstalledPaths(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
hostID uint,
|
|
|
|
|
) (
|
|
|
|
|
[]fleet.HostSoftwareInstalledPath,
|
|
|
|
|
error,
|
|
|
|
|
) {
|
|
|
|
|
stmt := `
|
|
|
|
|
SELECT t.id, t.host_id, t.software_id, t.installed_path
|
|
|
|
|
FROM host_software_installed_paths t
|
|
|
|
|
WHERE t.host_id = ?
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
var result []fleet.HostSoftwareInstalledPath
|
2023-06-19 17:55:15 +00:00
|
|
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &result, stmt, hostID); err != nil {
|
2023-05-17 18:49:09 +00:00
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// hostSoftwareInstalledPathsDelta returns what should be inserted and deleted to keep the
|
|
|
|
|
// 'host_software_installed_paths' table in-sync with the osquery reported query results.
|
|
|
|
|
// 'reported' is a set of 'installed_path-software.UniqueStr' strings, built from the osquery
|
|
|
|
|
// results.
|
|
|
|
|
// 'stored' contains all 'host_software_installed_paths' rows for the given host.
|
|
|
|
|
// 'hostSoftware' contains the current software installed on the host.
|
|
|
|
|
func hostSoftwareInstalledPathsDelta(
|
|
|
|
|
hostID uint,
|
|
|
|
|
reported map[string]struct{},
|
|
|
|
|
stored []fleet.HostSoftwareInstalledPath,
|
|
|
|
|
hostSoftware []fleet.Software,
|
|
|
|
|
) (
|
|
|
|
|
toInsert []fleet.HostSoftwareInstalledPath,
|
|
|
|
|
toDelete []uint,
|
|
|
|
|
err error,
|
|
|
|
|
) {
|
|
|
|
|
if len(reported) != 0 && len(hostSoftware) == 0 {
|
|
|
|
|
// Error condition, something reported implies that the host has some software
|
|
|
|
|
err = fmt.Errorf("software installed paths for host %d were reported but host contains no software", hostID)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sIDLookup := map[uint]fleet.Software{}
|
|
|
|
|
for _, s := range hostSoftware {
|
|
|
|
|
sIDLookup[s.ID] = s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sUnqStrLook := map[string]fleet.Software{}
|
|
|
|
|
for _, s := range hostSoftware {
|
|
|
|
|
sUnqStrLook[s.ToUniqueStr()] = s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
iSPathLookup := make(map[string]fleet.HostSoftwareInstalledPath)
|
|
|
|
|
for _, r := range stored {
|
|
|
|
|
s, ok := sIDLookup[r.SoftwareID]
|
|
|
|
|
// Software currently not found on the host, should be deleted ...
|
|
|
|
|
if !ok {
|
|
|
|
|
toDelete = append(toDelete, r.ID)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
key := fmt.Sprintf("%s%s%s", r.InstalledPath, fleet.SoftwareFieldSeparator, s.ToUniqueStr())
|
|
|
|
|
iSPathLookup[key] = r
|
|
|
|
|
|
|
|
|
|
// Anything stored but not reported should be deleted
|
|
|
|
|
if _, ok := reported[key]; !ok {
|
|
|
|
|
toDelete = append(toDelete, r.ID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for key := range reported {
|
|
|
|
|
parts := strings.SplitN(key, fleet.SoftwareFieldSeparator, 2)
|
|
|
|
|
iSPath, unqStr := parts[0], parts[1]
|
|
|
|
|
|
|
|
|
|
// Shouldn't be possible ... everything 'reported' should be in the the software table
|
|
|
|
|
// because this executes after 'ds.UpdateHostSoftware'
|
|
|
|
|
s, ok := sUnqStrLook[unqStr]
|
|
|
|
|
if !ok {
|
|
|
|
|
err = fmt.Errorf("reported installed path for %s does not belong to any stored software entry", unqStr)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, ok := iSPathLookup[key]; ok {
|
|
|
|
|
// Nothing to do
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toInsert = append(toInsert, fleet.HostSoftwareInstalledPath{
|
|
|
|
|
HostID: hostID,
|
|
|
|
|
SoftwareID: s.ID,
|
|
|
|
|
InstalledPath: iSPath,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func deleteHostSoftwareInstalledPaths(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
tx sqlx.ExtContext,
|
|
|
|
|
toDelete []uint,
|
|
|
|
|
) error {
|
|
|
|
|
if len(toDelete) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stmt := `DELETE FROM host_software_installed_paths WHERE id IN (?)`
|
|
|
|
|
stmt, args, err := sqlx.In(stmt, toDelete)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ctxerr.Wrap(ctx, err, "building delete statement for delete host_software_installed_paths")
|
|
|
|
|
}
|
|
|
|
|
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
|
|
|
|
|
return ctxerr.Wrap(ctx, err, "executing delete statement for delete host_software_installed_paths")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func insertHostSoftwareInstalledPaths(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
tx sqlx.ExtContext,
|
|
|
|
|
toInsert []fleet.HostSoftwareInstalledPath,
|
|
|
|
|
) error {
|
|
|
|
|
if len(toInsert) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stmt := "INSERT INTO host_software_installed_paths (host_id, software_id, installed_path) VALUES %s"
|
|
|
|
|
batchSize := 500
|
|
|
|
|
|
|
|
|
|
for i := 0; i < len(toInsert); i += batchSize {
|
|
|
|
|
end := i + batchSize
|
|
|
|
|
if end > len(toInsert) {
|
|
|
|
|
end = len(toInsert)
|
|
|
|
|
}
|
|
|
|
|
batch := toInsert[i:end]
|
|
|
|
|
|
|
|
|
|
var args []interface{}
|
|
|
|
|
for _, v := range batch {
|
|
|
|
|
args = append(args, v.HostID, v.SoftwareID, v.InstalledPath)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
placeHolders := strings.TrimSuffix(strings.Repeat("(?, ?, ?), ", len(batch)), ", ")
|
|
|
|
|
stmt := fmt.Sprintf(stmt, placeHolders)
|
|
|
|
|
|
|
|
|
|
_, err := tx.ExecContext(ctx, stmt, args...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ctxerr.Wrap(ctx, err, "inserting rows into host_software_installed_paths")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-27 13:47:09 +00:00
|
|
|
func nothingChanged(current, incoming []fleet.Software, minLastOpenedAtDiff time.Duration) bool {
|
2021-07-08 16:57:43 +00:00
|
|
|
if len(current) != len(incoming) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-26 18:16:59 +00:00
|
|
|
currentMap := make(map[string]fleet.Software)
|
2021-07-08 16:57:43 +00:00
|
|
|
for _, s := range current {
|
2023-05-17 18:49:09 +00:00
|
|
|
currentMap[s.ToUniqueStr()] = s
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
|
|
|
|
for _, s := range incoming {
|
2023-05-17 18:49:09 +00:00
|
|
|
cur, ok := currentMap[s.ToUniqueStr()]
|
2022-04-26 18:16:59 +00:00
|
|
|
if !ok {
|
2021-07-08 16:57:43 +00:00
|
|
|
return false
|
|
|
|
|
}
|
2022-04-26 18:16:59 +00:00
|
|
|
|
|
|
|
|
// if the incoming software has a last opened at timestamp and it differs
|
|
|
|
|
// significantly from the current timestamp (or there is no current
|
|
|
|
|
// timestamp), then consider that something changed.
|
|
|
|
|
if s.LastOpenedAt != nil {
|
|
|
|
|
if cur.LastOpenedAt == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
oldLast := *cur.LastOpenedAt
|
|
|
|
|
newLast := *s.LastOpenedAt
|
|
|
|
|
if newLast.Sub(oldLast) >= minLastOpenedAtDiff {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
func (ds *Datastore) ListSoftwareByHostIDShort(ctx context.Context, hostID uint) ([]fleet.Software, error) {
|
2023-06-19 17:55:15 +00:00
|
|
|
return listSoftwareByHostIDShort(ctx, ds.reader(ctx), hostID)
|
2022-05-20 16:58:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func listSoftwareByHostIDShort(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
db sqlx.QueryerContext,
|
|
|
|
|
hostID uint,
|
|
|
|
|
) ([]fleet.Software, error) {
|
|
|
|
|
q := `
|
|
|
|
|
SELECT
|
|
|
|
|
s.id,
|
|
|
|
|
s.name,
|
|
|
|
|
s.version,
|
|
|
|
|
s.source,
|
2023-12-07 23:43:37 +00:00
|
|
|
s.browser,
|
2022-05-20 16:58:40 +00:00
|
|
|
s.bundle_identifier,
|
|
|
|
|
s.release,
|
|
|
|
|
s.vendor,
|
|
|
|
|
s.arch,
|
2023-12-12 22:51:58 +00:00
|
|
|
s.extension_id,
|
2022-05-20 16:58:40 +00:00
|
|
|
hs.last_opened_at
|
|
|
|
|
FROM
|
|
|
|
|
software s
|
|
|
|
|
JOIN host_software hs ON hs.software_id = s.id
|
|
|
|
|
WHERE
|
|
|
|
|
hs.host_id = ?
|
|
|
|
|
`
|
|
|
|
|
var softwares []fleet.Software
|
|
|
|
|
err := sqlx.SelectContext(ctx, db, &softwares, q, hostID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return softwares, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-17 18:49:09 +00:00
|
|
|
// applyChangesForNewSoftwareDB returns the current host software and the applied mutations: what
|
|
|
|
|
// was inserted and what was deleted
|
2022-04-27 13:47:09 +00:00
|
|
|
func applyChangesForNewSoftwareDB(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
tx sqlx.ExtContext,
|
|
|
|
|
hostID uint,
|
|
|
|
|
software []fleet.Software,
|
|
|
|
|
minLastOpenedAtDiff time.Duration,
|
2023-05-17 18:49:09 +00:00
|
|
|
) (*fleet.UpdateHostSoftwareDBResult, error) {
|
|
|
|
|
r := &fleet.UpdateHostSoftwareDBResult{}
|
|
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
currentSoftware, err := listSoftwareByHostIDShort(ctx, tx, hostID)
|
2021-07-08 16:57:43 +00:00
|
|
|
if err != nil {
|
2023-05-17 18:49:09 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "loading current software for host")
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
2023-05-17 18:49:09 +00:00
|
|
|
r.WasCurrInstalled = currentSoftware
|
2021-07-08 16:57:43 +00:00
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
if nothingChanged(currentSoftware, software, minLastOpenedAtDiff) {
|
2023-05-24 19:05:45 +00:00
|
|
|
return r, nil
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
|
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
current := softwareSliceToMap(currentSoftware)
|
2022-04-26 18:16:59 +00:00
|
|
|
incoming := softwareSliceToMap(software)
|
2021-07-08 16:57:43 +00:00
|
|
|
|
2023-05-17 18:49:09 +00:00
|
|
|
deleted, err := deleteUninstalledHostSoftwareDB(ctx, tx, hostID, current, incoming)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
2023-05-17 18:49:09 +00:00
|
|
|
r.Deleted = deleted
|
2021-07-08 16:57:43 +00:00
|
|
|
|
2023-05-17 18:49:09 +00:00
|
|
|
inserted, err := insertNewInstalledHostSoftwareDB(ctx, tx, hostID, current, incoming)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
2023-05-17 18:49:09 +00:00
|
|
|
r.Inserted = inserted
|
2021-07-08 16:57:43 +00:00
|
|
|
|
2022-04-27 13:47:09 +00:00
|
|
|
if err = updateModifiedHostSoftwareDB(ctx, tx, hostID, current, incoming, minLastOpenedAtDiff); err != nil {
|
2023-05-17 18:49:09 +00:00
|
|
|
return nil, err
|
2022-04-26 18:16:59 +00:00
|
|
|
}
|
|
|
|
|
|
2023-01-09 11:55:43 +00:00
|
|
|
if err = updateSoftwareUpdatedAt(ctx, tx, hostID); err != nil {
|
2023-05-17 18:49:09 +00:00
|
|
|
return nil, err
|
2023-01-09 11:55:43 +00:00
|
|
|
}
|
|
|
|
|
|
2023-05-17 18:49:09 +00:00
|
|
|
return r, nil
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
|
|
|
|
|
2022-04-26 18:16:59 +00:00
|
|
|
// delete host_software that is in current map, but not in incoming map.
|
2023-05-17 18:49:09 +00:00
|
|
|
// returns the deleted software on the host
|
2021-09-08 18:43:22 +00:00
|
|
|
func deleteUninstalledHostSoftwareDB(
|
2021-09-14 14:44:02 +00:00
|
|
|
ctx context.Context,
|
|
|
|
|
tx sqlx.ExecerContext,
|
2021-07-08 16:57:43 +00:00
|
|
|
hostID uint,
|
2022-04-26 18:16:59 +00:00
|
|
|
currentMap map[string]fleet.Software,
|
|
|
|
|
incomingMap map[string]fleet.Software,
|
2023-05-17 18:49:09 +00:00
|
|
|
) ([]fleet.Software, error) {
|
|
|
|
|
var deletesHostSoftwareIDs []uint
|
|
|
|
|
var deletedSoftware []fleet.Software
|
|
|
|
|
|
2022-04-26 18:16:59 +00:00
|
|
|
for currentKey, curSw := range currentMap {
|
|
|
|
|
if _, ok := incomingMap[currentKey]; !ok {
|
2023-05-17 18:49:09 +00:00
|
|
|
deletedSoftware = append(deletedSoftware, curSw)
|
|
|
|
|
deletesHostSoftwareIDs = append(deletesHostSoftwareIDs, curSw.ID)
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
2023-05-17 18:49:09 +00:00
|
|
|
if len(deletesHostSoftwareIDs) == 0 {
|
|
|
|
|
return nil, nil
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
2023-04-05 16:53:43 +00:00
|
|
|
|
|
|
|
|
stmt := `DELETE FROM host_software WHERE host_id = ? AND software_id IN (?);`
|
2023-05-17 18:49:09 +00:00
|
|
|
stmt, args, err := sqlx.In(stmt, hostID, deletesHostSoftwareIDs)
|
2023-04-05 16:53:43 +00:00
|
|
|
if err != nil {
|
2023-05-17 18:49:09 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "build delete host software query")
|
2023-04-05 16:53:43 +00:00
|
|
|
}
|
|
|
|
|
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
|
2023-05-17 18:49:09 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "delete host software")
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
|
|
|
|
|
2023-05-17 18:49:09 +00:00
|
|
|
return deletedSoftware, nil
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
|
|
|
|
|
2021-09-14 14:44:02 +00:00
|
|
|
func getOrGenerateSoftwareIdDB(ctx context.Context, tx sqlx.ExtContext, s fleet.Software) (uint, error) {
|
2022-04-20 14:02:35 +00:00
|
|
|
getExistingID := func() (int64, error) {
|
|
|
|
|
var existingID int64
|
|
|
|
|
if err := sqlx.GetContext(ctx, tx, &existingID,
|
|
|
|
|
"SELECT id FROM software "+
|
|
|
|
|
"WHERE name = ? AND version = ? AND source = ? AND `release` = ? AND "+
|
2023-12-01 01:06:17 +00:00
|
|
|
"vendor = ? AND arch = ? AND bundle_identifier = ? AND extension_id = ? AND browser = ? LIMIT 1",
|
|
|
|
|
s.Name, s.Version, s.Source, s.Release, s.Vendor, s.Arch, s.BundleIdentifier, s.ExtensionID, s.Browser,
|
2022-04-20 14:02:35 +00:00
|
|
|
); err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
return existingID, nil
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
2022-04-20 14:02:35 +00:00
|
|
|
|
|
|
|
|
switch id, err := getExistingID(); {
|
|
|
|
|
case err == nil:
|
|
|
|
|
return uint(id), nil
|
|
|
|
|
case errors.Is(err, sql.ErrNoRows):
|
|
|
|
|
// OK
|
|
|
|
|
default:
|
|
|
|
|
return 0, ctxerr.Wrap(ctx, err, "get software")
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
|
|
|
|
|
2022-02-18 19:30:24 +00:00
|
|
|
_, err := tx.ExecContext(ctx,
|
2023-12-12 22:51:58 +00:00
|
|
|
fmt.Sprintf("INSERT INTO software "+
|
|
|
|
|
"(name, version, source, `release`, vendor, arch, bundle_identifier, extension_id, browser, checksum) "+
|
|
|
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, %s)", softwareChecksumComputedColumn("")),
|
2023-12-01 01:06:17 +00:00
|
|
|
s.Name, s.Version, s.Source, s.Release, s.Vendor, s.Arch, s.BundleIdentifier, s.ExtensionID, s.Browser,
|
2021-07-08 16:57:43 +00:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
2023-12-12 22:51:58 +00:00
|
|
|
if !isDuplicate(err) {
|
|
|
|
|
return 0, ctxerr.Wrap(ctx, err, "insert software")
|
|
|
|
|
}
|
|
|
|
|
// if the error is a duplicate software entry, there was a race and another
|
|
|
|
|
// process inserted that software, so continue and try to get its id as it
|
|
|
|
|
// now exists.
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
2022-04-20 14:02:35 +00:00
|
|
|
|
|
|
|
|
// LastInsertId sometimes returns 0 as it's dependent on connections and how mysql is
|
|
|
|
|
// configured.
|
|
|
|
|
switch id, err := getExistingID(); {
|
|
|
|
|
case err == nil:
|
|
|
|
|
return uint(id), nil
|
|
|
|
|
case errors.Is(err, sql.ErrNoRows):
|
|
|
|
|
return 0, doRetryErr
|
|
|
|
|
default:
|
|
|
|
|
return 0, ctxerr.Wrap(ctx, err, "get software")
|
|
|
|
|
}
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
|
|
|
|
|
2023-12-12 22:51:58 +00:00
|
|
|
func softwareChecksumComputedColumn(tableAlias string) string {
|
|
|
|
|
if tableAlias != "" && !strings.HasSuffix(tableAlias, ".") {
|
|
|
|
|
tableAlias += "."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// concatenate with separator \x00
|
|
|
|
|
return fmt.Sprintf(` UNHEX(
|
|
|
|
|
MD5(
|
|
|
|
|
CONCAT_WS(CHAR(0),
|
|
|
|
|
%sname,
|
|
|
|
|
%[1]sversion,
|
|
|
|
|
%[1]ssource,
|
|
|
|
|
COALESCE(%[1]sbundle_identifier, ''),
|
|
|
|
|
`+"%[1]s`release`"+`,
|
|
|
|
|
%[1]sarch,
|
|
|
|
|
%[1]svendor,
|
|
|
|
|
%[1]sbrowser,
|
|
|
|
|
%[1]sextension_id
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
) `, tableAlias)
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-26 18:16:59 +00:00
|
|
|
// insert host_software that is in incoming map, but not in current map.
|
2023-05-17 18:49:09 +00:00
|
|
|
// returns the inserted software on the host
|
2021-09-08 18:43:22 +00:00
|
|
|
func insertNewInstalledHostSoftwareDB(
|
2021-09-14 14:44:02 +00:00
|
|
|
ctx context.Context,
|
|
|
|
|
tx sqlx.ExtContext,
|
2021-07-08 16:57:43 +00:00
|
|
|
hostID uint,
|
2022-04-26 18:16:59 +00:00
|
|
|
currentMap map[string]fleet.Software,
|
|
|
|
|
incomingMap map[string]fleet.Software,
|
2023-05-17 18:49:09 +00:00
|
|
|
) ([]fleet.Software, error) {
|
2021-07-08 16:57:43 +00:00
|
|
|
var insertsHostSoftware []interface{}
|
2023-05-17 18:49:09 +00:00
|
|
|
var insertedSoftware []fleet.Software
|
2022-04-26 18:16:59 +00:00
|
|
|
|
2023-09-06 20:32:11 +00:00
|
|
|
type softwareWithUniqueName struct {
|
|
|
|
|
uniqueName string
|
|
|
|
|
software fleet.Software
|
2021-11-08 16:07:42 +00:00
|
|
|
}
|
2023-09-06 20:32:11 +00:00
|
|
|
incomingOrdered := make([]softwareWithUniqueName, 0, len(incomingMap))
|
|
|
|
|
for uniqueName, software := range incomingMap {
|
|
|
|
|
incomingOrdered = append(incomingOrdered, softwareWithUniqueName{
|
|
|
|
|
uniqueName: uniqueName,
|
|
|
|
|
software: software,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
sort.Slice(incomingOrdered, func(i, j int) bool {
|
|
|
|
|
return incomingOrdered[i].uniqueName < incomingOrdered[j].uniqueName
|
|
|
|
|
})
|
2022-04-26 18:16:59 +00:00
|
|
|
|
2021-11-08 16:07:42 +00:00
|
|
|
for _, s := range incomingOrdered {
|
2023-09-06 20:32:11 +00:00
|
|
|
if _, ok := currentMap[s.uniqueName]; !ok {
|
|
|
|
|
id, err := getOrGenerateSoftwareIdDB(ctx, tx, s.software)
|
2021-07-08 16:57:43 +00:00
|
|
|
if err != nil {
|
2023-05-17 18:49:09 +00:00
|
|
|
return nil, err
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
2023-09-06 20:32:11 +00:00
|
|
|
insertsHostSoftware = append(insertsHostSoftware, hostID, id, s.software.LastOpenedAt)
|
2023-05-17 18:49:09 +00:00
|
|
|
|
2023-09-06 20:32:11 +00:00
|
|
|
s.software.ID = id
|
|
|
|
|
insertedSoftware = append(insertedSoftware, s.software)
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
2022-04-26 18:16:59 +00:00
|
|
|
|
2021-07-08 16:57:43 +00:00
|
|
|
if len(insertsHostSoftware) > 0 {
|
2022-04-26 18:16:59 +00:00
|
|
|
values := strings.TrimSuffix(strings.Repeat("(?,?,?),", len(insertsHostSoftware)/3), ",")
|
|
|
|
|
sql := fmt.Sprintf(`INSERT IGNORE INTO host_software (host_id, software_id, last_opened_at) VALUES %s`, values)
|
2021-09-14 14:44:02 +00:00
|
|
|
if _, err := tx.ExecContext(ctx, sql, insertsHostSoftware...); err != nil {
|
2023-05-17 18:49:09 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "insert host software")
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-17 18:49:09 +00:00
|
|
|
return insertedSoftware, nil
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
|
|
|
|
|
2022-04-26 18:16:59 +00:00
|
|
|
// update host_software when incoming software has a significantly more recent
|
|
|
|
|
// last opened timestamp (or didn't have on in currentMap). Note that it only
|
|
|
|
|
// processes software that is in both current and incoming maps, as the case
|
|
|
|
|
// where it is only in incoming is already handled by
|
|
|
|
|
// insertNewInstalledHostSoftwareDB.
|
|
|
|
|
func updateModifiedHostSoftwareDB(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
tx sqlx.ExtContext,
|
|
|
|
|
hostID uint,
|
|
|
|
|
currentMap map[string]fleet.Software,
|
|
|
|
|
incomingMap map[string]fleet.Software,
|
2022-04-27 13:47:09 +00:00
|
|
|
minLastOpenedAtDiff time.Duration,
|
2022-04-26 18:16:59 +00:00
|
|
|
) error {
|
|
|
|
|
const stmt = `UPDATE host_software SET last_opened_at = ? WHERE host_id = ? AND software_id = ?`
|
|
|
|
|
|
|
|
|
|
var keysToUpdate []string
|
|
|
|
|
for key, newSw := range incomingMap {
|
|
|
|
|
curSw, ok := currentMap[key]
|
|
|
|
|
if !ok || newSw.LastOpenedAt == nil {
|
|
|
|
|
// software must also exist in current map, and new software must have a
|
|
|
|
|
// last opened at timestamp (otherwise we don't overwrite the old one)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if curSw.LastOpenedAt == nil || (*newSw.LastOpenedAt).Sub(*curSw.LastOpenedAt) >= minLastOpenedAtDiff {
|
|
|
|
|
keysToUpdate = append(keysToUpdate, key)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sort.Strings(keysToUpdate)
|
|
|
|
|
|
|
|
|
|
for _, key := range keysToUpdate {
|
|
|
|
|
curSw, newSw := currentMap[key], incomingMap[key]
|
|
|
|
|
if _, err := tx.ExecContext(ctx, stmt, newSw.LastOpenedAt, hostID, curSw.ID); err != nil {
|
|
|
|
|
return ctxerr.Wrap(ctx, err, "update host software")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-09 11:55:43 +00:00
|
|
|
func updateSoftwareUpdatedAt(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
tx sqlx.ExtContext,
|
|
|
|
|
hostID uint,
|
|
|
|
|
) error {
|
|
|
|
|
const stmt = `INSERT INTO host_updates(host_id, software_updated_at) VALUES (?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE software_updated_at=VALUES(software_updated_at)`
|
|
|
|
|
|
|
|
|
|
if _, err := tx.ExecContext(ctx, stmt, hostID); err != nil {
|
|
|
|
|
return ctxerr.Wrap(ctx, err, "update host updates")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-04 18:21:39 +00:00
|
|
|
var dialect = goqu.Dialect("mysql")
|
|
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
// listSoftwareDB returns software installed on hosts. Use opts for pagination, filtering, and controlling
|
|
|
|
|
// fields populated in the returned software.
|
2021-11-04 18:21:39 +00:00
|
|
|
func listSoftwareDB(
|
2022-05-20 16:58:40 +00:00
|
|
|
ctx context.Context,
|
|
|
|
|
q sqlx.QueryerContext,
|
|
|
|
|
opts fleet.SoftwareListOptions,
|
2021-11-04 18:21:39 +00:00
|
|
|
) ([]fleet.Software, error) {
|
2022-05-20 16:58:40 +00:00
|
|
|
sql, args, err := selectSoftwareSQL(opts)
|
2021-12-03 13:54:17 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "sql build")
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
var results []softwareCVE
|
|
|
|
|
if err := sqlx.SelectContext(ctx, q, &results, sql, args...); err != nil {
|
2022-02-09 15:16:50 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "select host software")
|
2021-12-03 13:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
var softwares []fleet.Software
|
|
|
|
|
ids := make(map[uint]int) // map of ids to index into softwares
|
|
|
|
|
for _, result := range results {
|
|
|
|
|
result := result // create a copy because we need to take the address to fields below
|
2021-12-03 13:54:17 +00:00
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
idx, ok := ids[result.ID]
|
|
|
|
|
if !ok {
|
|
|
|
|
idx = len(softwares)
|
|
|
|
|
softwares = append(softwares, result.Software)
|
|
|
|
|
ids[result.ID] = idx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handle null cve from left join
|
|
|
|
|
if result.CVE != nil {
|
|
|
|
|
cveID := *result.CVE
|
|
|
|
|
cve := fleet.CVE{
|
|
|
|
|
CVE: cveID,
|
|
|
|
|
DetailsLink: fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", cveID),
|
|
|
|
|
}
|
|
|
|
|
if opts.IncludeCVEScores {
|
|
|
|
|
cve.CVSSScore = &result.CVSSScore
|
|
|
|
|
cve.EPSSProbability = &result.EPSSProbability
|
|
|
|
|
cve.CISAKnownExploit = &result.CISAKnownExploit
|
2023-03-28 20:11:31 +00:00
|
|
|
cve.CVEPublished = &result.CVEPublished
|
2023-09-15 17:24:10 +00:00
|
|
|
cve.Description = &result.Description
|
2023-09-18 22:53:32 +00:00
|
|
|
cve.ResolvedInVersion = &result.ResolvedInVersion
|
2022-05-20 16:58:40 +00:00
|
|
|
}
|
|
|
|
|
softwares[idx].Vulnerabilities = append(softwares[idx].Vulnerabilities, cve)
|
|
|
|
|
}
|
2021-12-03 13:54:17 +00:00
|
|
|
}
|
2022-05-20 16:58:40 +00:00
|
|
|
|
|
|
|
|
return softwares, nil
|
2021-12-03 13:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
// softwareCVE is used for left joins with cve
|
2023-09-18 22:53:32 +00:00
|
|
|
//
|
|
|
|
|
//
|
|
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
type softwareCVE struct {
|
|
|
|
|
fleet.Software
|
2023-09-18 22:53:32 +00:00
|
|
|
|
|
|
|
|
// CVE is the CVE identifier pulled from the NVD json (e.g. CVE-2019-1234)
|
|
|
|
|
CVE *string `db:"cve"`
|
|
|
|
|
|
|
|
|
|
// CVSSScore is the CVSS score pulled from the NVD json (premium only)
|
|
|
|
|
CVSSScore *float64 `db:"cvss_score"`
|
|
|
|
|
|
|
|
|
|
// EPSSProbability is the EPSS probability pulled from FIRST (premium only)
|
|
|
|
|
EPSSProbability *float64 `db:"epss_probability"`
|
|
|
|
|
|
|
|
|
|
// CISAKnownExploit is the CISAKnownExploit pulled from CISA (premium only)
|
|
|
|
|
CISAKnownExploit *bool `db:"cisa_known_exploit"`
|
|
|
|
|
|
|
|
|
|
// CVEPublished is the CVE published date pulled from the NVD json (premium only)
|
|
|
|
|
CVEPublished *time.Time `db:"cve_published"`
|
|
|
|
|
|
|
|
|
|
// Description is the CVE description field pulled from the NVD json
|
|
|
|
|
Description *string `db:"description"`
|
|
|
|
|
|
|
|
|
|
// ResolvedInVersion is the version of software where the CVE is no longer applicable.
|
|
|
|
|
// This is pulled from the versionEndExcluding field in the NVD json
|
|
|
|
|
ResolvedInVersion *string `db:"resolved_in_version"`
|
2022-05-20 16:58:40 +00:00
|
|
|
}
|
2021-09-14 13:58:48 +00:00
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
func selectSoftwareSQL(opts fleet.SoftwareListOptions) (string, []interface{}, error) {
|
|
|
|
|
ds := dialect.
|
|
|
|
|
From(goqu.I("software").As("s")).
|
|
|
|
|
Select(
|
|
|
|
|
"s.id",
|
|
|
|
|
"s.name",
|
|
|
|
|
"s.version",
|
|
|
|
|
"s.source",
|
|
|
|
|
"s.bundle_identifier",
|
2023-12-01 01:06:17 +00:00
|
|
|
"s.extension_id",
|
|
|
|
|
"s.browser",
|
2022-05-20 16:58:40 +00:00
|
|
|
"s.release",
|
|
|
|
|
"s.vendor",
|
|
|
|
|
"s.arch",
|
2022-08-04 13:24:44 +00:00
|
|
|
goqu.I("scp.cpe").As("generated_cpe"),
|
2022-05-30 15:23:27 +00:00
|
|
|
).
|
2022-08-04 13:24:44 +00:00
|
|
|
// Include this in the sub-query in case we want to sort by 'generated_cpe'
|
|
|
|
|
LeftJoin(
|
|
|
|
|
goqu.I("software_cpe").As("scp"),
|
|
|
|
|
goqu.On(
|
|
|
|
|
goqu.I("s.id").Eq(goqu.I("scp.software_id")),
|
|
|
|
|
),
|
2021-12-09 20:36:12 +00:00
|
|
|
)
|
2022-05-20 16:58:40 +00:00
|
|
|
|
|
|
|
|
if opts.HostID != nil {
|
|
|
|
|
ds = ds.
|
2022-08-10 21:43:22 +00:00
|
|
|
Join(
|
|
|
|
|
goqu.I("host_software").As("hs"),
|
|
|
|
|
goqu.On(
|
|
|
|
|
goqu.I("hs.software_id").Eq(goqu.I("s.id")),
|
|
|
|
|
goqu.I("hs.host_id").Eq(opts.HostID),
|
|
|
|
|
),
|
|
|
|
|
).
|
|
|
|
|
SelectAppend("hs.last_opened_at")
|
|
|
|
|
if opts.TeamID != nil {
|
|
|
|
|
ds = ds.
|
|
|
|
|
Join(
|
|
|
|
|
goqu.I("hosts").As("h"),
|
|
|
|
|
goqu.On(
|
|
|
|
|
goqu.I("hs.host_id").Eq(goqu.I("h.id")),
|
|
|
|
|
goqu.I("h.team_id").Eq(opts.TeamID),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
2021-12-09 20:36:12 +00:00
|
|
|
|
2022-08-10 21:43:22 +00:00
|
|
|
} else {
|
|
|
|
|
// When loading software from all hosts, filter out software that is not associated with any
|
|
|
|
|
// hosts.
|
2022-05-20 16:58:40 +00:00
|
|
|
ds = ds.
|
|
|
|
|
Join(
|
2022-08-10 21:43:22 +00:00
|
|
|
goqu.I("software_host_counts").As("shc"),
|
2022-05-20 16:58:40 +00:00
|
|
|
goqu.On(
|
2022-08-10 21:43:22 +00:00
|
|
|
goqu.I("s.id").Eq(goqu.I("shc.software_id")),
|
|
|
|
|
goqu.I("shc.hosts_count").Gt(0),
|
2022-05-20 16:58:40 +00:00
|
|
|
),
|
|
|
|
|
).
|
2022-08-10 21:43:22 +00:00
|
|
|
GroupByAppend(
|
|
|
|
|
"shc.hosts_count",
|
|
|
|
|
"shc.updated_at",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if opts.TeamID != nil {
|
|
|
|
|
ds = ds.Where(goqu.I("shc.team_id").Eq(opts.TeamID))
|
|
|
|
|
} else {
|
|
|
|
|
ds = ds.Where(goqu.I("shc.team_id").Eq(0))
|
|
|
|
|
}
|
2021-11-04 18:21:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if opts.VulnerableOnly {
|
2022-05-20 16:58:40 +00:00
|
|
|
ds = ds.
|
|
|
|
|
Join(
|
|
|
|
|
goqu.I("software_cve").As("scv"),
|
2022-08-04 13:24:44 +00:00
|
|
|
goqu.On(goqu.I("s.id").Eq(goqu.I("scv.software_id"))),
|
2022-05-20 16:58:40 +00:00
|
|
|
)
|
2021-11-04 18:21:39 +00:00
|
|
|
} else {
|
2022-05-20 16:58:40 +00:00
|
|
|
ds = ds.
|
|
|
|
|
LeftJoin(
|
2022-01-28 13:05:11 +00:00
|
|
|
goqu.I("software_cve").As("scv"),
|
2022-08-04 13:24:44 +00:00
|
|
|
goqu.On(goqu.I("s.id").Eq(goqu.I("scv.software_id"))),
|
2022-01-28 13:05:11 +00:00
|
|
|
)
|
2022-05-20 16:58:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if opts.IncludeCVEScores {
|
|
|
|
|
ds = ds.
|
|
|
|
|
LeftJoin(
|
2022-06-01 16:06:57 +00:00
|
|
|
goqu.I("cve_meta").As("c"),
|
2022-05-20 16:58:40 +00:00
|
|
|
goqu.On(goqu.I("c.cve").Eq(goqu.I("scv.cve"))),
|
|
|
|
|
).
|
|
|
|
|
SelectAppend(
|
2023-09-18 22:53:32 +00:00
|
|
|
goqu.MAX("c.cvss_score").As("cvss_score"), // for ordering
|
|
|
|
|
goqu.MAX("c.epss_probability").As("epss_probability"), // for ordering
|
|
|
|
|
goqu.MAX("c.cisa_known_exploit").As("cisa_known_exploit"), // for ordering
|
|
|
|
|
goqu.MAX("c.published").As("cve_published"), // for ordering
|
|
|
|
|
goqu.MAX("c.description").As("description"), // for ordering
|
|
|
|
|
goqu.MAX("scv.resolved_in_version").As("resolved_in_version"), // for ordering
|
2022-05-20 16:58:40 +00:00
|
|
|
)
|
2022-01-28 13:05:11 +00:00
|
|
|
}
|
|
|
|
|
|
2023-11-01 14:56:27 +00:00
|
|
|
if match := opts.ListOptions.MatchQuery; match != "" {
|
2022-01-28 13:05:11 +00:00
|
|
|
match = likePattern(match)
|
|
|
|
|
ds = ds.Where(
|
|
|
|
|
goqu.Or(
|
|
|
|
|
goqu.I("s.name").ILike(match),
|
|
|
|
|
goqu.I("s.version").ILike(match),
|
|
|
|
|
goqu.I("scv.cve").ILike(match),
|
|
|
|
|
),
|
|
|
|
|
)
|
2021-11-04 18:21:39 +00:00
|
|
|
}
|
|
|
|
|
|
2022-01-26 14:47:56 +00:00
|
|
|
if opts.WithHostCounts {
|
2022-05-20 16:58:40 +00:00
|
|
|
ds = ds.
|
2022-02-09 15:16:50 +00:00
|
|
|
SelectAppend(
|
|
|
|
|
goqu.I("shc.hosts_count"),
|
|
|
|
|
goqu.I("shc.updated_at").As("counts_updated_at"),
|
|
|
|
|
)
|
2022-01-26 14:47:56 +00:00
|
|
|
}
|
2022-05-20 16:58:40 +00:00
|
|
|
|
|
|
|
|
ds = ds.GroupBy(
|
|
|
|
|
"s.id",
|
2022-08-04 13:24:44 +00:00
|
|
|
"s.name",
|
|
|
|
|
"s.version",
|
|
|
|
|
"s.source",
|
|
|
|
|
"s.bundle_identifier",
|
2023-12-01 01:06:17 +00:00
|
|
|
"s.extension_id",
|
|
|
|
|
"s.browser",
|
2022-08-04 13:24:44 +00:00
|
|
|
"s.release",
|
|
|
|
|
"s.vendor",
|
|
|
|
|
"s.arch",
|
2022-05-20 16:58:40 +00:00
|
|
|
"generated_cpe",
|
|
|
|
|
)
|
|
|
|
|
|
2022-08-04 13:24:44 +00:00
|
|
|
// Pagination is a bit more complex here due to the join with software_cve table and aggregated columns from cve_meta table.
|
2022-05-20 16:58:40 +00:00
|
|
|
// Apply order by again after joining on sub query
|
2022-02-09 15:16:50 +00:00
|
|
|
ds = appendListOptionsToSelect(ds, opts.ListOptions)
|
2022-01-26 14:47:56 +00:00
|
|
|
|
2022-06-01 16:06:57 +00:00
|
|
|
// join on software_cve and cve_meta after apply pagination using the sub-query above
|
2022-05-20 16:58:40 +00:00
|
|
|
ds = dialect.From(ds.As("s")).
|
|
|
|
|
Select(
|
|
|
|
|
"s.id",
|
|
|
|
|
"s.name",
|
|
|
|
|
"s.version",
|
|
|
|
|
"s.source",
|
|
|
|
|
"s.bundle_identifier",
|
2023-12-01 01:06:17 +00:00
|
|
|
"s.extension_id",
|
|
|
|
|
"s.browser",
|
2022-05-20 16:58:40 +00:00
|
|
|
"s.release",
|
|
|
|
|
"s.vendor",
|
|
|
|
|
"s.arch",
|
2022-08-04 13:24:44 +00:00
|
|
|
goqu.COALESCE(goqu.I("s.generated_cpe"), "").As("generated_cpe"),
|
2022-05-20 16:58:40 +00:00
|
|
|
"scv.cve",
|
|
|
|
|
).
|
|
|
|
|
LeftJoin(
|
|
|
|
|
goqu.I("software_cve").As("scv"),
|
2022-08-04 13:24:44 +00:00
|
|
|
goqu.On(goqu.I("scv.software_id").Eq(goqu.I("s.id"))),
|
2022-05-20 16:58:40 +00:00
|
|
|
).
|
|
|
|
|
LeftJoin(
|
2022-06-01 16:06:57 +00:00
|
|
|
goqu.I("cve_meta").As("c"),
|
2022-05-20 16:58:40 +00:00
|
|
|
goqu.On(goqu.I("c.cve").Eq(goqu.I("scv.cve"))),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// select optional columns
|
|
|
|
|
if opts.IncludeCVEScores {
|
|
|
|
|
ds = ds.SelectAppend(
|
|
|
|
|
"c.cvss_score",
|
|
|
|
|
"c.epss_probability",
|
|
|
|
|
"c.cisa_known_exploit",
|
2023-09-15 17:24:10 +00:00
|
|
|
"c.description",
|
2023-03-28 20:11:31 +00:00
|
|
|
goqu.I("c.published").As("cve_published"),
|
2023-09-18 22:53:32 +00:00
|
|
|
"scv.resolved_in_version",
|
2022-05-20 16:58:40 +00:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if opts.HostID != nil {
|
|
|
|
|
ds = ds.SelectAppend(
|
|
|
|
|
goqu.I("s.last_opened_at"),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if opts.WithHostCounts {
|
|
|
|
|
ds = ds.SelectAppend(
|
|
|
|
|
goqu.I("s.hosts_count"),
|
|
|
|
|
goqu.I("s.counts_updated_at"),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ds = appendOrderByToSelect(ds, opts.ListOptions)
|
|
|
|
|
|
2021-12-03 13:54:17 +00:00
|
|
|
return ds.ToSQL()
|
|
|
|
|
}
|
2021-08-06 17:04:37 +00:00
|
|
|
|
2021-12-03 13:54:17 +00:00
|
|
|
func countSoftwareDB(
|
2022-05-20 16:58:40 +00:00
|
|
|
ctx context.Context,
|
|
|
|
|
q sqlx.QueryerContext,
|
|
|
|
|
opts fleet.SoftwareListOptions,
|
2021-12-03 13:54:17 +00:00
|
|
|
) (int, error) {
|
|
|
|
|
opts.ListOptions = fleet.ListOptions{
|
2023-11-01 14:56:27 +00:00
|
|
|
MatchQuery: opts.ListOptions.MatchQuery,
|
2021-10-20 22:26:25 +00:00
|
|
|
}
|
2022-05-20 16:58:40 +00:00
|
|
|
|
|
|
|
|
sql, args, err := selectSoftwareSQL(opts)
|
2021-11-04 18:21:39 +00:00
|
|
|
if err != nil {
|
2021-12-03 13:54:17 +00:00
|
|
|
return 0, ctxerr.Wrap(ctx, err, "sql build")
|
2021-11-04 18:21:39 +00:00
|
|
|
}
|
2021-12-03 13:54:17 +00:00
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
sql = `SELECT COUNT(DISTINCT s.id) FROM (` + sql + `) AS s`
|
2021-08-30 19:07:24 +00:00
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
var count int
|
|
|
|
|
if err := sqlx.GetContext(ctx, q, &count, sql, args...); err != nil {
|
|
|
|
|
return 0, ctxerr.Wrap(ctx, err, "count host software")
|
2021-08-30 19:07:24 +00:00
|
|
|
}
|
|
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
return count, nil
|
2021-07-08 16:57:43 +00:00
|
|
|
}
|
2021-04-26 15:44:22 +00:00
|
|
|
|
2022-06-01 16:06:57 +00:00
|
|
|
func (ds *Datastore) LoadHostSoftware(ctx context.Context, host *fleet.Host, includeCVEScores bool) error {
|
|
|
|
|
opts := fleet.SoftwareListOptions{
|
|
|
|
|
HostID: &host.ID,
|
|
|
|
|
IncludeCVEScores: includeCVEScores,
|
|
|
|
|
}
|
2023-06-19 17:55:15 +00:00
|
|
|
software, err := listSoftwareDB(ctx, ds.reader(ctx), opts)
|
2021-07-08 16:57:43 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2023-05-17 20:53:15 +00:00
|
|
|
|
|
|
|
|
installedPaths, err := ds.getHostSoftwareInstalledPaths(
|
|
|
|
|
ctx,
|
|
|
|
|
host.ID,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lookup := make(map[uint][]string)
|
|
|
|
|
for _, ip := range installedPaths {
|
|
|
|
|
lookup[ip.SoftwareID] = append(lookup[ip.SoftwareID], ip.InstalledPath)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
host.Software = make([]fleet.HostSoftwareEntry, 0, len(software))
|
|
|
|
|
for _, s := range software {
|
|
|
|
|
host.Software = append(host.Software, fleet.HostSoftwareEntry{
|
|
|
|
|
Software: s,
|
|
|
|
|
InstalledPaths: lookup[s.ID],
|
|
|
|
|
})
|
|
|
|
|
}
|
2021-04-26 15:44:22 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
2021-07-29 16:10:34 +00:00
|
|
|
|
|
|
|
|
type softwareIterator struct {
|
|
|
|
|
rows *sqlx.Rows
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (si *softwareIterator) Value() (*fleet.Software, error) {
|
|
|
|
|
dest := fleet.Software{}
|
|
|
|
|
err := si.rows.StructScan(&dest)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &dest, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (si *softwareIterator) Err() error {
|
|
|
|
|
return si.rows.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (si *softwareIterator) Close() error {
|
|
|
|
|
return si.rows.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (si *softwareIterator) Next() bool {
|
|
|
|
|
return si.rows.Next()
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-03 17:45:18 +00:00
|
|
|
// AllSoftwareIterator Returns an iterator for the 'software' table, filtering out
|
|
|
|
|
// software entries based on the 'query' param. The rows.Close call is done by the caller once
|
|
|
|
|
// iteration using the returned fleet.SoftwareIterator is done.
|
|
|
|
|
func (ds *Datastore) AllSoftwareIterator(
|
2023-02-24 18:18:25 +00:00
|
|
|
ctx context.Context,
|
2023-04-03 17:45:18 +00:00
|
|
|
query fleet.SoftwareIterQueryOptions,
|
2023-02-24 18:18:25 +00:00
|
|
|
) (fleet.SoftwareIterator, error) {
|
2023-04-03 17:45:18 +00:00
|
|
|
if !query.IsValid() {
|
|
|
|
|
return nil, fmt.Errorf("invalid query params %+v", query)
|
2023-02-24 18:18:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
var args []interface{}
|
|
|
|
|
|
2023-06-19 17:55:15 +00:00
|
|
|
stmt := `SELECT
|
2023-12-12 22:51:58 +00:00
|
|
|
s.id, s.name, s.version, s.source, s.bundle_identifier, s.release, s.arch, s.vendor, s.browser, s.extension_id, s.title_id ,
|
2023-04-03 17:45:18 +00:00
|
|
|
COALESCE(sc.cpe, '') AS generated_cpe
|
2023-06-19 17:55:15 +00:00
|
|
|
FROM software s
|
2023-04-03 17:45:18 +00:00
|
|
|
LEFT JOIN software_cpe sc ON (s.id=sc.software_id)`
|
2023-02-24 18:18:25 +00:00
|
|
|
|
2023-04-03 17:45:18 +00:00
|
|
|
var conditionals []string
|
|
|
|
|
arg := map[string]interface{}{}
|
2023-02-24 18:18:25 +00:00
|
|
|
|
2023-04-03 17:45:18 +00:00
|
|
|
if len(query.ExcludedSources) != 0 {
|
|
|
|
|
conditionals = append(conditionals, "s.source NOT IN (:excluded_sources)")
|
|
|
|
|
arg["excluded_sources"] = query.ExcludedSources
|
2023-02-24 18:18:25 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-03 17:45:18 +00:00
|
|
|
if len(query.IncludedSources) != 0 {
|
|
|
|
|
conditionals = append(conditionals, "s.source IN (:included_sources)")
|
|
|
|
|
arg["included_sources"] = query.IncludedSources
|
|
|
|
|
}
|
2022-08-04 13:24:44 +00:00
|
|
|
|
2023-04-03 17:45:18 +00:00
|
|
|
if len(conditionals) != 0 {
|
|
|
|
|
cond := strings.Join(conditionals, " AND ")
|
|
|
|
|
stmt, args, err = sqlx.Named(stmt+" WHERE "+cond, arg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "error binding named arguments on software iterator")
|
|
|
|
|
}
|
|
|
|
|
stmt, args, err = sqlx.In(stmt, args...)
|
2022-08-04 13:24:44 +00:00
|
|
|
if err != nil {
|
2023-04-03 17:45:18 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "error building 'In' query part on software iterator")
|
2022-08-04 13:24:44 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-19 17:55:15 +00:00
|
|
|
rows, err := ds.reader(ctx).QueryxContext(ctx, stmt, args...) //nolint:sqlclosecheck
|
2021-07-29 16:10:34 +00:00
|
|
|
if err != nil {
|
2021-11-15 14:11:38 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "load host software")
|
2021-07-29 16:10:34 +00:00
|
|
|
}
|
|
|
|
|
return &softwareIterator{rows: rows}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-03 17:45:18 +00:00
|
|
|
func (ds *Datastore) UpsertSoftwareCPEs(ctx context.Context, cpes []fleet.SoftwareCPE) (int64, error) {
|
|
|
|
|
var args []interface{}
|
|
|
|
|
|
|
|
|
|
if len(cpes) == 0 {
|
|
|
|
|
return 0, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
values := strings.TrimSuffix(strings.Repeat("(?,?),", len(cpes)), ",")
|
|
|
|
|
sql := fmt.Sprintf(
|
|
|
|
|
`INSERT INTO software_cpe (software_id, cpe) VALUES %s ON DUPLICATE KEY UPDATE cpe = VALUES(cpe)`,
|
|
|
|
|
values,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for _, cpe := range cpes {
|
|
|
|
|
args = append(args, cpe.SoftwareID, cpe.CPE)
|
|
|
|
|
}
|
2023-06-19 17:55:15 +00:00
|
|
|
res, err := ds.writer(ctx).ExecContext(ctx, sql, args...)
|
2023-04-03 17:45:18 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return 0, ctxerr.Wrap(ctx, err, "insert software cpes")
|
|
|
|
|
}
|
|
|
|
|
count, _ := res.RowsAffected()
|
|
|
|
|
|
|
|
|
|
return count, nil
|
2021-09-20 18:09:38 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-03 17:45:18 +00:00
|
|
|
func (ds *Datastore) DeleteSoftwareCPEs(ctx context.Context, cpes []fleet.SoftwareCPE) (int64, error) {
|
|
|
|
|
if len(cpes) == 0 {
|
|
|
|
|
return 0, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stmt := `DELETE FROM software_cpe WHERE (software_id) IN (?)`
|
|
|
|
|
|
|
|
|
|
softwareIDs := make([]uint, 0, len(cpes))
|
|
|
|
|
for _, cpe := range cpes {
|
|
|
|
|
softwareIDs = append(softwareIDs, cpe.SoftwareID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query, args, err := sqlx.In(stmt, softwareIDs)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, ctxerr.Wrap(ctx, err, "error building 'In' query part when deleting software CPEs")
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-19 17:55:15 +00:00
|
|
|
res, err := ds.writer(ctx).ExecContext(ctx, query, args...)
|
2021-09-20 18:09:38 +00:00
|
|
|
if err != nil {
|
2023-04-03 17:45:18 +00:00
|
|
|
return 0, ctxerr.Wrapf(ctx, err, "deleting cpes software")
|
2021-07-29 16:10:34 +00:00
|
|
|
}
|
2023-04-03 17:45:18 +00:00
|
|
|
|
|
|
|
|
count, _ := res.RowsAffected()
|
|
|
|
|
|
|
|
|
|
return count, nil
|
2021-07-29 16:10:34 +00:00
|
|
|
}
|
2021-08-04 21:01:39 +00:00
|
|
|
|
2022-08-24 17:10:58 +00:00
|
|
|
func (ds *Datastore) ListSoftwareCPEs(ctx context.Context) ([]fleet.SoftwareCPE, error) {
|
2022-06-08 01:09:47 +00:00
|
|
|
var result []fleet.SoftwareCPE
|
2021-08-04 21:01:39 +00:00
|
|
|
|
2022-06-08 01:09:47 +00:00
|
|
|
var err error
|
|
|
|
|
var args []interface{}
|
2022-02-18 18:25:26 +00:00
|
|
|
|
2022-06-08 01:09:47 +00:00
|
|
|
stmt := `SELECT id, software_id, cpe FROM software_cpe`
|
2023-06-19 17:55:15 +00:00
|
|
|
err = sqlx.SelectContext(ctx, ds.reader(ctx), &result, stmt, args...)
|
2022-02-18 18:25:26 +00:00
|
|
|
|
2022-06-08 01:09:47 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "loads cpes")
|
|
|
|
|
}
|
|
|
|
|
return result, nil
|
2021-08-04 21:01:39 +00:00
|
|
|
}
|
2021-09-14 13:58:48 +00:00
|
|
|
|
2023-12-12 18:24:20 +00:00
|
|
|
func (ds *Datastore) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) {
|
|
|
|
|
software, err := listSoftwareDB(ctx, ds.reader(ctx), opt)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
perPage := opt.ListOptions.PerPage
|
|
|
|
|
var metaData *fleet.PaginationMetadata
|
|
|
|
|
if opt.ListOptions.IncludeMetadata {
|
|
|
|
|
if perPage <= 0 {
|
|
|
|
|
perPage = defaultSelectLimit
|
|
|
|
|
}
|
|
|
|
|
metaData = &fleet.PaginationMetadata{HasPreviousResults: opt.ListOptions.Page > 0}
|
|
|
|
|
if len(software) > int(perPage) {
|
|
|
|
|
metaData.HasNextResults = true
|
|
|
|
|
software = software[:len(software)-1]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return software, metaData, nil
|
2021-09-14 13:58:48 +00:00
|
|
|
}
|
2021-10-12 18:59:01 +00:00
|
|
|
|
2022-02-03 17:56:22 +00:00
|
|
|
func (ds *Datastore) CountSoftware(ctx context.Context, opt fleet.SoftwareListOptions) (int, error) {
|
2023-06-19 17:55:15 +00:00
|
|
|
return countSoftwareDB(ctx, ds.reader(ctx), opt)
|
2021-12-03 13:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
2022-06-23 20:44:45 +00:00
|
|
|
// DeleteSoftwareVulnerabilities deletes the given list of software vulnerabilities
|
|
|
|
|
func (ds *Datastore) DeleteSoftwareVulnerabilities(ctx context.Context, vulnerabilities []fleet.SoftwareVulnerability) error {
|
2022-02-14 18:13:44 +00:00
|
|
|
if len(vulnerabilities) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sql := fmt.Sprintf(
|
2022-08-04 13:24:44 +00:00
|
|
|
`DELETE FROM software_cve WHERE (software_id, cve) IN (%s)`,
|
2022-02-14 18:13:44 +00:00
|
|
|
strings.TrimSuffix(strings.Repeat("(?,?),", len(vulnerabilities)), ","),
|
|
|
|
|
)
|
|
|
|
|
var args []interface{}
|
|
|
|
|
for _, vulnerability := range vulnerabilities {
|
2022-08-04 13:24:44 +00:00
|
|
|
args = append(args, vulnerability.SoftwareID, vulnerability.CVE)
|
2022-02-14 18:13:44 +00:00
|
|
|
}
|
2023-06-19 17:55:15 +00:00
|
|
|
if _, err := ds.writer(ctx).ExecContext(ctx, sql, args...); err != nil {
|
2022-02-14 18:13:44 +00:00
|
|
|
return ctxerr.Wrapf(ctx, err, "deleting vulnerable software")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-03 17:45:18 +00:00
|
|
|
func (ds *Datastore) DeleteOutOfDateVulnerabilities(ctx context.Context, source fleet.VulnerabilitySource, duration time.Duration) error {
|
|
|
|
|
sql := `DELETE FROM software_cve WHERE source = ? AND updated_at < ?`
|
|
|
|
|
|
|
|
|
|
var args []interface{}
|
|
|
|
|
cutPoint := time.Now().UTC().Add(-1 * duration)
|
|
|
|
|
args = append(args, source, cutPoint)
|
|
|
|
|
|
2023-06-19 17:55:15 +00:00
|
|
|
if _, err := ds.writer(ctx).ExecContext(ctx, sql, args...); err != nil {
|
2023-04-03 17:45:18 +00:00
|
|
|
return ctxerr.Wrap(ctx, err, "deleting out of date vulnerabilities")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-21 18:42:21 +00:00
|
|
|
func (ds *Datastore) SoftwareByID(ctx context.Context, id uint, teamID *uint, includeCVEScores bool, tmFilter *fleet.TeamFilter) (*fleet.Software, error) {
|
2022-05-20 16:58:40 +00:00
|
|
|
q := dialect.From(goqu.I("software").As("s")).
|
|
|
|
|
Select(
|
|
|
|
|
"s.id",
|
|
|
|
|
"s.name",
|
|
|
|
|
"s.version",
|
|
|
|
|
"s.source",
|
2023-12-06 14:30:49 +00:00
|
|
|
"s.browser",
|
2022-05-20 16:58:40 +00:00
|
|
|
"s.bundle_identifier",
|
|
|
|
|
"s.release",
|
|
|
|
|
"s.vendor",
|
|
|
|
|
"s.arch",
|
2023-12-12 22:51:58 +00:00
|
|
|
"s.extension_id",
|
2022-05-20 16:58:40 +00:00
|
|
|
"scv.cve",
|
2022-10-04 11:04:48 +00:00
|
|
|
goqu.COALESCE(goqu.I("scp.cpe"), "").As("generated_cpe"),
|
2022-05-20 16:58:40 +00:00
|
|
|
).
|
|
|
|
|
LeftJoin(
|
|
|
|
|
goqu.I("software_cpe").As("scp"),
|
|
|
|
|
goqu.On(
|
|
|
|
|
goqu.I("s.id").Eq(goqu.I("scp.software_id")),
|
|
|
|
|
),
|
|
|
|
|
).
|
|
|
|
|
LeftJoin(
|
|
|
|
|
goqu.I("software_cve").As("scv"),
|
2022-08-04 13:24:44 +00:00
|
|
|
goqu.On(goqu.I("s.id").Eq(goqu.I("scv.software_id"))),
|
2022-05-20 16:58:40 +00:00
|
|
|
)
|
|
|
|
|
|
2024-02-15 20:22:27 +00:00
|
|
|
if tmFilter != nil {
|
|
|
|
|
q = q.LeftJoin(
|
|
|
|
|
goqu.I("software_host_counts").As("shc"),
|
|
|
|
|
goqu.On(goqu.I("s.id").Eq(goqu.I("shc.software_id"))),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
if includeCVEScores {
|
|
|
|
|
q = q.
|
|
|
|
|
LeftJoin(
|
2022-06-01 16:06:57 +00:00
|
|
|
goqu.I("cve_meta").As("c"),
|
2022-05-20 16:58:40 +00:00
|
|
|
goqu.On(goqu.I("c.cve").Eq(goqu.I("scv.cve"))),
|
|
|
|
|
).
|
|
|
|
|
SelectAppend(
|
|
|
|
|
"c.cvss_score",
|
|
|
|
|
"c.epss_probability",
|
|
|
|
|
"c.cisa_known_exploit",
|
2023-09-15 17:24:10 +00:00
|
|
|
"c.description",
|
2023-03-28 20:11:31 +00:00
|
|
|
goqu.I("c.published").As("cve_published"),
|
2023-09-18 22:53:32 +00:00
|
|
|
"scv.resolved_in_version",
|
2022-05-20 16:58:40 +00:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
q = q.Where(goqu.I("s.id").Eq(id))
|
2022-08-24 13:45:20 +00:00
|
|
|
// filter software that is not associated with any hosts
|
2024-02-18 13:14:20 +00:00
|
|
|
if teamID == nil {
|
|
|
|
|
q = q.Where(goqu.L("EXISTS (SELECT 1 FROM host_software WHERE software_id = ? LIMIT 1)", id))
|
|
|
|
|
} else {
|
|
|
|
|
// if teamID filter is used, host counts need to be up-to-date
|
|
|
|
|
q = q.Where(
|
|
|
|
|
goqu.L(
|
|
|
|
|
"EXISTS (SELECT 1 FROM software_host_counts WHERE software_id = ? AND team_id = ? AND hosts_count > 0)", id, *teamID,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
2022-05-20 16:58:40 +00:00
|
|
|
|
2024-02-15 20:22:27 +00:00
|
|
|
// filter by teams
|
|
|
|
|
if tmFilter != nil {
|
|
|
|
|
q = q.Where(goqu.L(ds.whereFilterGlobalOrTeamIDByTeams(*tmFilter, "shc")))
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
sql, args, err := q.ToSQL()
|
2021-10-12 18:59:01 +00:00
|
|
|
if err != nil {
|
2022-05-20 16:58:40 +00:00
|
|
|
return nil, err
|
2021-10-12 18:59:01 +00:00
|
|
|
}
|
2021-10-14 16:51:41 +00:00
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
var results []softwareCVE
|
2023-06-19 17:55:15 +00:00
|
|
|
err = sqlx.SelectContext(ctx, ds.reader(ctx), &results, sql, args...)
|
2021-10-14 16:51:41 +00:00
|
|
|
if err != nil {
|
2022-05-20 16:58:40 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "get software")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(results) == 0 {
|
|
|
|
|
return nil, ctxerr.Wrap(ctx, notFound("Software").WithID(id))
|
2021-10-14 16:51:41 +00:00
|
|
|
}
|
|
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
var software fleet.Software
|
|
|
|
|
for i, result := range results {
|
|
|
|
|
result := result // create a copy because we need to take the address to fields below
|
|
|
|
|
|
|
|
|
|
if i == 0 {
|
|
|
|
|
software = result.Software
|
2021-10-14 16:51:41 +00:00
|
|
|
}
|
|
|
|
|
|
2022-05-20 16:58:40 +00:00
|
|
|
if result.CVE != nil {
|
|
|
|
|
cveID := *result.CVE
|
|
|
|
|
cve := fleet.CVE{
|
|
|
|
|
CVE: cveID,
|
|
|
|
|
DetailsLink: fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", cveID),
|
|
|
|
|
}
|
|
|
|
|
if includeCVEScores {
|
|
|
|
|
cve.CVSSScore = &result.CVSSScore
|
|
|
|
|
cve.EPSSProbability = &result.EPSSProbability
|
|
|
|
|
cve.CISAKnownExploit = &result.CISAKnownExploit
|
2023-03-28 20:11:31 +00:00
|
|
|
cve.CVEPublished = &result.CVEPublished
|
2023-09-18 22:53:32 +00:00
|
|
|
cve.ResolvedInVersion = &result.ResolvedInVersion
|
2022-05-20 16:58:40 +00:00
|
|
|
}
|
|
|
|
|
software.Vulnerabilities = append(software.Vulnerabilities, cve)
|
|
|
|
|
}
|
2021-10-14 16:51:41 +00:00
|
|
|
}
|
|
|
|
|
|
2021-10-12 18:59:01 +00:00
|
|
|
return &software, nil
|
|
|
|
|
}
|
2022-01-26 14:47:56 +00:00
|
|
|
|
2022-06-22 20:35:53 +00:00
|
|
|
// SyncHostsSoftware calculates the number of hosts having each
|
2022-02-09 15:16:50 +00:00
|
|
|
// software installed and stores that information in the software_host_counts
|
2022-01-26 14:47:56 +00:00
|
|
|
// table.
|
2022-02-14 18:13:44 +00:00
|
|
|
//
|
|
|
|
|
// After aggregation, it cleans up unused software (e.g. software installed
|
|
|
|
|
// on removed hosts, software uninstalled on hosts, etc.)
|
2022-06-22 20:35:53 +00:00
|
|
|
func (ds *Datastore) SyncHostsSoftware(ctx context.Context, updatedAt time.Time) error {
|
2022-02-28 18:55:14 +00:00
|
|
|
const (
|
|
|
|
|
resetStmt = `
|
|
|
|
|
UPDATE software_host_counts
|
|
|
|
|
SET hosts_count = 0, updated_at = ?`
|
|
|
|
|
|
|
|
|
|
// team_id is added to the select list to have the same structure as
|
|
|
|
|
// the teamCountsStmt, making it easier to use a common implementation
|
|
|
|
|
globalCountsStmt = `
|
|
|
|
|
SELECT count(*), 0 as team_id, software_id
|
|
|
|
|
FROM host_software
|
2024-05-08 14:27:17 +00:00
|
|
|
WHERE software_id > ? AND software_id <= ?
|
2022-02-28 18:55:14 +00:00
|
|
|
GROUP BY software_id`
|
|
|
|
|
|
|
|
|
|
teamCountsStmt = `
|
|
|
|
|
SELECT count(*), h.team_id, hs.software_id
|
|
|
|
|
FROM host_software hs
|
|
|
|
|
INNER JOIN hosts h
|
|
|
|
|
ON hs.host_id = h.id
|
2024-05-08 14:27:17 +00:00
|
|
|
WHERE h.team_id IS NOT NULL AND hs.software_id > ? AND hs.software_id <= ?
|
2022-02-28 18:55:14 +00:00
|
|
|
GROUP BY hs.software_id, h.team_id`
|
|
|
|
|
|
|
|
|
|
insertStmt = `
|
|
|
|
|
INSERT INTO software_host_counts
|
|
|
|
|
(software_id, hosts_count, team_id, updated_at)
|
|
|
|
|
VALUES
|
|
|
|
|
%s
|
|
|
|
|
ON DUPLICATE KEY UPDATE
|
|
|
|
|
hosts_count = VALUES(hosts_count),
|
|
|
|
|
updated_at = VALUES(updated_at)`
|
|
|
|
|
|
|
|
|
|
valuesPart = `(?, ?, ?, ?),`
|
|
|
|
|
|
|
|
|
|
cleanupSoftwareStmt = `
|
|
|
|
|
DELETE s
|
|
|
|
|
FROM software s
|
|
|
|
|
LEFT JOIN software_host_counts shc
|
|
|
|
|
ON s.id = shc.software_id
|
|
|
|
|
WHERE
|
|
|
|
|
shc.software_id IS NULL OR
|
|
|
|
|
(shc.team_id = 0 AND shc.hosts_count = 0)`
|
|
|
|
|
|
2022-06-22 20:35:53 +00:00
|
|
|
cleanupOrphanedStmt = `
|
|
|
|
|
DELETE shc
|
|
|
|
|
FROM
|
|
|
|
|
software_host_counts shc
|
|
|
|
|
LEFT JOIN software s ON s.id = shc.software_id
|
|
|
|
|
WHERE
|
|
|
|
|
s.id IS NULL
|
|
|
|
|
`
|
|
|
|
|
|
2022-02-28 18:55:14 +00:00
|
|
|
cleanupTeamStmt = `
|
|
|
|
|
DELETE shc
|
|
|
|
|
FROM software_host_counts shc
|
|
|
|
|
LEFT JOIN teams t
|
|
|
|
|
ON t.id = shc.team_id
|
|
|
|
|
WHERE
|
|
|
|
|
shc.team_id > 0 AND
|
|
|
|
|
t.id IS NULL`
|
|
|
|
|
)
|
2022-01-26 14:47:56 +00:00
|
|
|
|
|
|
|
|
// first, reset all counts to 0
|
2023-06-19 17:55:15 +00:00
|
|
|
if _, err := ds.writer(ctx).ExecContext(ctx, resetStmt, updatedAt); err != nil {
|
2022-02-09 15:16:50 +00:00
|
|
|
return ctxerr.Wrap(ctx, err, "reset all software_host_counts to 0")
|
2022-01-26 14:47:56 +00:00
|
|
|
}
|
|
|
|
|
|
2024-05-08 14:27:17 +00:00
|
|
|
db := ds.reader(ctx)
|
2022-01-26 14:47:56 +00:00
|
|
|
|
2024-05-08 14:27:17 +00:00
|
|
|
// Figure out how many software items we need to count.
|
|
|
|
|
type minMaxIDs struct {
|
|
|
|
|
Min uint64 `db:"min"`
|
|
|
|
|
Max uint64 `db:"max"`
|
|
|
|
|
}
|
|
|
|
|
minMax := minMaxIDs{}
|
|
|
|
|
err := sqlx.GetContext(
|
|
|
|
|
ctx, db, &minMax, "SELECT COALESCE(MIN(software_id),1) as min, COALESCE(MAX(software_id),0) as max FROM host_software",
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ctxerr.Wrap(ctx, err, "get min/max software_id")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for minSoftwareID, maxSoftwareID := minMax.Min-1, minMax.Min-1+countHostSoftwareBatchSize; minSoftwareID < minMax.Max; minSoftwareID, maxSoftwareID = maxSoftwareID, maxSoftwareID+countHostSoftwareBatchSize {
|
|
|
|
|
|
|
|
|
|
// next get a cursor for the global and team counts for each software
|
|
|
|
|
stmtLabel := []string{"global", "team"}
|
|
|
|
|
for i, countStmt := range []string{globalCountsStmt, teamCountsStmt} {
|
|
|
|
|
rows, err := db.QueryContext(ctx, countStmt, minSoftwareID, maxSoftwareID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ctxerr.Wrapf(ctx, err, "read %s counts from host_software", stmtLabel[i])
|
2022-02-28 18:55:14 +00:00
|
|
|
}
|
2024-05-08 14:27:17 +00:00
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
|
|
// use a loop to iterate to prevent loading all in one go in memory, as it
|
|
|
|
|
// could get pretty big at >100K hosts with 1000+ software each. Use a write
|
|
|
|
|
// batch to prevent making too many single-row inserts.
|
|
|
|
|
const batchSize = 100
|
|
|
|
|
var batchCount int
|
|
|
|
|
args := make([]interface{}, 0, batchSize*4)
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var (
|
|
|
|
|
count int
|
|
|
|
|
teamID uint
|
|
|
|
|
sid uint
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if err := rows.Scan(&count, &teamID, &sid); err != nil {
|
|
|
|
|
return ctxerr.Wrapf(ctx, err, "scan %s row into variables", stmtLabel[i])
|
|
|
|
|
}
|
2022-01-26 14:47:56 +00:00
|
|
|
|
2024-05-08 14:27:17 +00:00
|
|
|
args = append(args, sid, count, teamID, updatedAt)
|
|
|
|
|
batchCount++
|
2022-01-26 14:47:56 +00:00
|
|
|
|
2024-05-08 14:27:17 +00:00
|
|
|
if batchCount == batchSize {
|
|
|
|
|
values := strings.TrimSuffix(strings.Repeat(valuesPart, batchCount), ",")
|
|
|
|
|
if _, err := ds.writer(ctx).ExecContext(ctx, fmt.Sprintf(insertStmt, values), args...); err != nil {
|
|
|
|
|
return ctxerr.Wrapf(ctx, err, "insert %s batch into software_host_counts", stmtLabel[i])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
args = args[:0]
|
|
|
|
|
batchCount = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if batchCount > 0 {
|
2022-02-28 18:55:14 +00:00
|
|
|
values := strings.TrimSuffix(strings.Repeat(valuesPart, batchCount), ",")
|
2023-06-19 17:55:15 +00:00
|
|
|
if _, err := ds.writer(ctx).ExecContext(ctx, fmt.Sprintf(insertStmt, values), args...); err != nil {
|
2024-05-08 14:27:17 +00:00
|
|
|
return ctxerr.Wrapf(ctx, err, "insert last %s batch into software_host_counts", stmtLabel[i])
|
2022-02-28 18:55:14 +00:00
|
|
|
}
|
|
|
|
|
}
|
2024-05-08 14:27:17 +00:00
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
|
return ctxerr.Wrapf(ctx, err, "iterate over %s host_software counts", stmtLabel[i])
|
2022-01-26 14:47:56 +00:00
|
|
|
}
|
2024-05-08 14:27:17 +00:00
|
|
|
rows.Close()
|
2022-01-26 14:47:56 +00:00
|
|
|
}
|
2024-05-08 14:27:17 +00:00
|
|
|
|
2022-01-26 14:47:56 +00:00
|
|
|
}
|
|
|
|
|
|
2022-02-28 18:55:14 +00:00
|
|
|
// remove any unused software (global counts = 0)
|
2023-06-19 17:55:15 +00:00
|
|
|
if _, err := ds.writer(ctx).ExecContext(ctx, cleanupSoftwareStmt); err != nil {
|
2022-01-26 16:32:42 +00:00
|
|
|
return ctxerr.Wrap(ctx, err, "delete unused software")
|
|
|
|
|
}
|
2022-02-28 18:55:14 +00:00
|
|
|
|
2022-06-22 20:35:53 +00:00
|
|
|
// remove any software count row for software that don't exist anymore
|
2023-06-19 17:55:15 +00:00
|
|
|
if _, err := ds.writer(ctx).ExecContext(ctx, cleanupOrphanedStmt); err != nil {
|
2023-12-13 15:48:57 +00:00
|
|
|
return ctxerr.Wrap(ctx, err, "delete software_host_counts for non-existing software")
|
2022-06-22 20:35:53 +00:00
|
|
|
}
|
|
|
|
|
|
2022-02-28 18:55:14 +00:00
|
|
|
// remove any software count row for teams that don't exist anymore
|
2023-06-19 17:55:15 +00:00
|
|
|
if _, err := ds.writer(ctx).ExecContext(ctx, cleanupTeamStmt); err != nil {
|
2022-02-28 18:55:14 +00:00
|
|
|
return ctxerr.Wrap(ctx, err, "delete software_host_counts for non-existing teams")
|
|
|
|
|
}
|
2022-01-26 14:47:56 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
2022-02-02 21:34:37 +00:00
|
|
|
|
2023-12-04 16:09:23 +00:00
|
|
|
func (ds *Datastore) ReconcileSoftwareTitles(ctx context.Context) error {
|
|
|
|
|
// TODO: consider if we should batch writes to software or software_titles table
|
|
|
|
|
|
|
|
|
|
// ensure all software titles are in the software_titles table
|
|
|
|
|
upsertTitlesStmt := `
|
2023-12-07 23:43:37 +00:00
|
|
|
INSERT INTO software_titles (name, source, browser)
|
2023-12-04 16:09:23 +00:00
|
|
|
SELECT DISTINCT
|
|
|
|
|
name,
|
2023-12-07 23:43:37 +00:00
|
|
|
source,
|
|
|
|
|
browser
|
2023-12-04 16:09:23 +00:00
|
|
|
FROM
|
|
|
|
|
software s
|
2023-12-12 22:51:58 +00:00
|
|
|
WHERE
|
|
|
|
|
NOT EXISTS (SELECT 1 FROM software_titles st WHERE (s.name, s.source, s.browser) = (st.name, st.source, st.browser))
|
2023-12-04 16:09:23 +00:00
|
|
|
ON DUPLICATE KEY UPDATE software_titles.id = software_titles.id`
|
|
|
|
|
// TODO: consider the impact of on duplicate key update vs. risk of insert ignore
|
|
|
|
|
// or performing a select first to see if the title exists and only inserting
|
|
|
|
|
// new titles
|
|
|
|
|
|
|
|
|
|
res, err := ds.writer(ctx).ExecContext(ctx, upsertTitlesStmt)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ctxerr.Wrap(ctx, err, "upsert software titles")
|
|
|
|
|
}
|
|
|
|
|
n, _ := res.RowsAffected()
|
|
|
|
|
level.Debug(ds.logger).Log("msg", "upsert software titles", "rows_affected", n)
|
|
|
|
|
|
|
|
|
|
// update title ids for software table entries
|
|
|
|
|
updateSoftwareStmt := `
|
|
|
|
|
UPDATE
|
|
|
|
|
software s,
|
|
|
|
|
software_titles st
|
|
|
|
|
SET
|
|
|
|
|
s.title_id = st.id
|
|
|
|
|
WHERE
|
2023-12-07 23:43:37 +00:00
|
|
|
(s.name, s.source, s.browser) = (st.name, st.source, st.browser)
|
2023-12-04 16:09:23 +00:00
|
|
|
AND (s.title_id IS NULL OR s.title_id != st.id)`
|
|
|
|
|
|
|
|
|
|
res, err = ds.writer(ctx).ExecContext(ctx, updateSoftwareStmt)
|
|
|
|
|
if err != nil {
|
2023-12-07 23:43:37 +00:00
|
|
|
return ctxerr.Wrap(ctx, err, "update software title_id")
|
2023-12-04 16:09:23 +00:00
|
|
|
}
|
|
|
|
|
n, _ = res.RowsAffected()
|
2023-12-07 23:43:37 +00:00
|
|
|
level.Debug(ds.logger).Log("msg", "update software title_id", "rows_affected", n)
|
2023-12-04 16:09:23 +00:00
|
|
|
|
|
|
|
|
// clean up orphaned software titles
|
|
|
|
|
cleanupStmt := `
|
2023-12-12 22:51:58 +00:00
|
|
|
DELETE st FROM software_titles st
|
|
|
|
|
LEFT JOIN software s ON s.title_id = st.id
|
2023-12-04 16:09:23 +00:00
|
|
|
WHERE s.title_id IS NULL`
|
|
|
|
|
|
|
|
|
|
res, err = ds.writer(ctx).ExecContext(ctx, cleanupStmt)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ctxerr.Wrap(ctx, err, "cleanup orphaned software titles")
|
|
|
|
|
}
|
|
|
|
|
n, _ = res.RowsAffected()
|
|
|
|
|
level.Debug(ds.logger).Log("msg", "cleanup orphaned software titles", "rows_affected", n)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-17 20:53:15 +00:00
|
|
|
func (ds *Datastore) HostVulnSummariesBySoftwareIDs(ctx context.Context, softwareIDs []uint) ([]fleet.HostVulnerabilitySummary, error) {
|
|
|
|
|
stmt := `
|
2023-06-19 17:55:15 +00:00
|
|
|
SELECT DISTINCT
|
2023-05-17 20:53:15 +00:00
|
|
|
h.id,
|
|
|
|
|
h.hostname,
|
|
|
|
|
if(h.computer_name = '', h.hostname, h.computer_name) display_name,
|
|
|
|
|
COALESCE(hsip.installed_path, '') AS software_installed_path
|
|
|
|
|
FROM hosts h
|
|
|
|
|
INNER JOIN host_software hs ON h.id = hs.host_id AND hs.software_id IN (?)
|
|
|
|
|
LEFT JOIN host_software_installed_paths hsip ON hs.host_id = hsip.host_id AND hs.software_id = hsip.software_id
|
|
|
|
|
ORDER BY h.id`
|
|
|
|
|
|
|
|
|
|
stmt, args, err := sqlx.In(stmt, softwareIDs)
|
2022-02-02 21:34:37 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "building query args")
|
|
|
|
|
}
|
2023-05-17 20:53:15 +00:00
|
|
|
|
|
|
|
|
var qR []struct {
|
|
|
|
|
HostID uint `db:"id"`
|
|
|
|
|
HostName string `db:"hostname"`
|
|
|
|
|
DisplayName string `db:"display_name"`
|
|
|
|
|
SPath string `db:"software_installed_path"`
|
2022-02-02 21:34:37 +00:00
|
|
|
}
|
2023-06-19 17:55:15 +00:00
|
|
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &qR, stmt, args...); err != nil {
|
2023-05-17 20:53:15 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "selecting hosts by softwareIDs")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result []fleet.HostVulnerabilitySummary
|
|
|
|
|
lookup := make(map[uint]int)
|
|
|
|
|
|
|
|
|
|
for _, r := range qR {
|
|
|
|
|
i, ok := lookup[r.HostID]
|
|
|
|
|
|
|
|
|
|
if ok {
|
|
|
|
|
result[i].AddSoftwareInstalledPath(r.SPath)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mapped := fleet.HostVulnerabilitySummary{
|
|
|
|
|
ID: r.HostID,
|
|
|
|
|
Hostname: r.HostName,
|
|
|
|
|
DisplayName: r.DisplayName,
|
|
|
|
|
}
|
|
|
|
|
mapped.AddSoftwareInstalledPath(r.SPath)
|
|
|
|
|
result = append(result, mapped)
|
|
|
|
|
|
|
|
|
|
lookup[r.HostID] = len(result) - 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result, nil
|
2022-02-02 21:34:37 +00:00
|
|
|
}
|
2022-04-11 20:42:16 +00:00
|
|
|
|
2023-05-17 20:53:15 +00:00
|
|
|
// ** DEPRECATED **
|
|
|
|
|
func (ds *Datastore) HostsByCVE(ctx context.Context, cve string) ([]fleet.HostVulnerabilitySummary, error) {
|
|
|
|
|
stmt := `
|
|
|
|
|
SELECT DISTINCT
|
|
|
|
|
(h.id),
|
|
|
|
|
h.hostname,
|
|
|
|
|
if(h.computer_name = '', h.hostname, h.computer_name) display_name,
|
|
|
|
|
COALESCE(hsip.installed_path, '') AS software_installed_path
|
|
|
|
|
FROM hosts h
|
|
|
|
|
INNER JOIN host_software hs ON h.id = hs.host_id
|
|
|
|
|
INNER JOIN software_cve scv ON scv.software_id = hs.software_id
|
|
|
|
|
LEFT JOIN host_software_installed_paths hsip ON hs.host_id = hsip.host_id AND hs.software_id = hsip.software_id
|
|
|
|
|
WHERE scv.cve = ?
|
|
|
|
|
ORDER BY h.id`
|
|
|
|
|
|
|
|
|
|
var qR []struct {
|
|
|
|
|
HostID uint `db:"id"`
|
|
|
|
|
HostName string `db:"hostname"`
|
|
|
|
|
DisplayName string `db:"display_name"`
|
|
|
|
|
SPath string `db:"software_installed_path"`
|
|
|
|
|
}
|
2023-06-19 17:55:15 +00:00
|
|
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &qR, stmt, cve); err != nil {
|
2023-05-17 20:53:15 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "selecting hosts by softwareIDs")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result []fleet.HostVulnerabilitySummary
|
|
|
|
|
lookup := make(map[uint]int)
|
|
|
|
|
|
|
|
|
|
for _, r := range qR {
|
|
|
|
|
i, ok := lookup[r.HostID]
|
|
|
|
|
|
|
|
|
|
if ok {
|
|
|
|
|
result[i].AddSoftwareInstalledPath(r.SPath)
|
|
|
|
|
continue
|
|
|
|
|
}
|
2022-04-11 20:42:16 +00:00
|
|
|
|
2023-05-17 20:53:15 +00:00
|
|
|
mapped := fleet.HostVulnerabilitySummary{
|
|
|
|
|
ID: r.HostID,
|
|
|
|
|
Hostname: r.HostName,
|
|
|
|
|
DisplayName: r.DisplayName,
|
|
|
|
|
}
|
|
|
|
|
mapped.AddSoftwareInstalledPath(r.SPath)
|
|
|
|
|
result = append(result, mapped)
|
|
|
|
|
|
|
|
|
|
lookup[r.HostID] = len(result) - 1
|
2022-04-11 20:42:16 +00:00
|
|
|
}
|
2023-05-17 20:53:15 +00:00
|
|
|
|
|
|
|
|
return result, nil
|
2022-04-11 20:42:16 +00:00
|
|
|
}
|
2022-05-20 16:58:40 +00:00
|
|
|
|
2022-06-01 16:06:57 +00:00
|
|
|
func (ds *Datastore) InsertCVEMeta(ctx context.Context, cveMeta []fleet.CVEMeta) error {
|
2022-05-20 16:58:40 +00:00
|
|
|
query := `
|
2023-09-15 17:24:10 +00:00
|
|
|
INSERT INTO cve_meta (cve, cvss_score, epss_probability, cisa_known_exploit, published, description)
|
2022-05-20 16:58:40 +00:00
|
|
|
VALUES %s
|
|
|
|
|
ON DUPLICATE KEY UPDATE
|
|
|
|
|
cvss_score = VALUES(cvss_score),
|
|
|
|
|
epss_probability = VALUES(epss_probability),
|
2022-06-01 16:06:57 +00:00
|
|
|
cisa_known_exploit = VALUES(cisa_known_exploit),
|
2023-09-15 17:24:10 +00:00
|
|
|
published = VALUES(published),
|
|
|
|
|
description = VALUES(description)
|
2022-05-20 16:58:40 +00:00
|
|
|
`
|
|
|
|
|
|
|
|
|
|
batchSize := 500
|
2022-06-01 16:06:57 +00:00
|
|
|
for i := 0; i < len(cveMeta); i += batchSize {
|
2022-05-20 16:58:40 +00:00
|
|
|
end := i + batchSize
|
2022-06-01 16:06:57 +00:00
|
|
|
if end > len(cveMeta) {
|
|
|
|
|
end = len(cveMeta)
|
2022-05-20 16:58:40 +00:00
|
|
|
}
|
|
|
|
|
|
2022-06-01 16:06:57 +00:00
|
|
|
batch := cveMeta[i:end]
|
2022-05-20 16:58:40 +00:00
|
|
|
|
2023-09-15 17:24:10 +00:00
|
|
|
valuesFrag := strings.TrimSuffix(strings.Repeat("(?, ?, ?, ?, ?, ?), ", len(batch)), ", ")
|
2022-05-20 16:58:40 +00:00
|
|
|
var args []interface{}
|
2022-06-01 16:06:57 +00:00
|
|
|
for _, meta := range batch {
|
2023-09-15 17:24:10 +00:00
|
|
|
args = append(args, meta.CVE, meta.CVSSScore, meta.EPSSProbability, meta.CISAKnownExploit, meta.Published, meta.Description)
|
2022-05-20 16:58:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query := fmt.Sprintf(query, valuesFrag)
|
|
|
|
|
|
2023-06-19 17:55:15 +00:00
|
|
|
_, err := ds.writer(ctx).ExecContext(ctx, query, args...)
|
2022-05-20 16:58:40 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return ctxerr.Wrap(ctx, err, "insert cve scores")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2022-06-08 01:09:47 +00:00
|
|
|
|
2023-04-03 17:45:18 +00:00
|
|
|
func (ds *Datastore) InsertSoftwareVulnerability(
|
2022-06-08 01:09:47 +00:00
|
|
|
ctx context.Context,
|
2023-04-03 17:45:18 +00:00
|
|
|
vuln fleet.SoftwareVulnerability,
|
2022-06-08 01:09:47 +00:00
|
|
|
source fleet.VulnerabilitySource,
|
2023-04-03 17:45:18 +00:00
|
|
|
) (bool, error) {
|
|
|
|
|
if vuln.CVE == "" {
|
|
|
|
|
return false, nil
|
2022-06-08 01:09:47 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-03 17:45:18 +00:00
|
|
|
var args []interface{}
|
2022-06-08 01:09:47 +00:00
|
|
|
|
2023-09-18 22:53:32 +00:00
|
|
|
stmt := `
|
2023-12-12 22:51:58 +00:00
|
|
|
INSERT INTO software_cve (cve, source, software_id, resolved_in_version)
|
|
|
|
|
VALUES (?,?,?,?)
|
2023-09-18 22:53:32 +00:00
|
|
|
ON DUPLICATE KEY UPDATE
|
|
|
|
|
source = VALUES(source),
|
|
|
|
|
resolved_in_version = VALUES(resolved_in_version),
|
|
|
|
|
updated_at=?
|
|
|
|
|
`
|
|
|
|
|
args = append(args, vuln.CVE, source, vuln.SoftwareID, vuln.ResolvedInVersion, time.Now().UTC())
|
2023-04-03 17:45:18 +00:00
|
|
|
|
2023-06-19 17:55:15 +00:00
|
|
|
res, err := ds.writer(ctx).ExecContext(ctx, stmt, args...)
|
2022-06-08 01:09:47 +00:00
|
|
|
if err != nil {
|
2023-04-03 17:45:18 +00:00
|
|
|
return false, ctxerr.Wrap(ctx, err, "insert software vulnerability")
|
2022-06-08 01:09:47 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-03 17:45:18 +00:00
|
|
|
return insertOnDuplicateDidInsert(res), nil
|
2022-06-08 01:09:47 +00:00
|
|
|
}
|
|
|
|
|
|
2022-11-10 17:28:00 +00:00
|
|
|
func (ds *Datastore) ListSoftwareVulnerabilitiesByHostIDsSource(
|
2022-06-08 01:09:47 +00:00
|
|
|
ctx context.Context,
|
|
|
|
|
hostIDs []uint,
|
2022-11-10 17:28:00 +00:00
|
|
|
source fleet.VulnerabilitySource,
|
2022-06-08 01:09:47 +00:00
|
|
|
) (map[uint][]fleet.SoftwareVulnerability, error) {
|
|
|
|
|
result := make(map[uint][]fleet.SoftwareVulnerability)
|
|
|
|
|
|
|
|
|
|
type softwareVulnerabilityWithHostId struct {
|
|
|
|
|
fleet.SoftwareVulnerability
|
2022-11-10 17:28:00 +00:00
|
|
|
HostID uint `db:"host_id"`
|
2022-06-08 01:09:47 +00:00
|
|
|
}
|
|
|
|
|
var queryR []softwareVulnerabilityWithHostId
|
|
|
|
|
|
|
|
|
|
stmt := dialect.
|
2022-11-10 17:28:00 +00:00
|
|
|
From(goqu.T("software_cve").As("sc")).
|
2022-06-08 01:09:47 +00:00
|
|
|
Join(
|
|
|
|
|
goqu.T("host_software").As("hs"),
|
|
|
|
|
goqu.On(goqu.Ex{
|
2022-11-10 17:28:00 +00:00
|
|
|
"sc.software_id": goqu.I("hs.software_id"),
|
2022-06-08 01:09:47 +00:00
|
|
|
}),
|
|
|
|
|
).
|
|
|
|
|
Select(
|
2022-11-10 17:28:00 +00:00
|
|
|
goqu.I("hs.host_id"),
|
|
|
|
|
goqu.I("sc.software_id"),
|
|
|
|
|
goqu.I("sc.cve"),
|
2023-09-18 22:53:32 +00:00
|
|
|
goqu.I("sc.resolved_in_version"),
|
2022-06-08 01:09:47 +00:00
|
|
|
).
|
2022-11-10 17:28:00 +00:00
|
|
|
Where(
|
|
|
|
|
goqu.I("hs.host_id").In(hostIDs),
|
|
|
|
|
goqu.I("sc.source").Eq(source),
|
|
|
|
|
)
|
2022-06-08 01:09:47 +00:00
|
|
|
|
|
|
|
|
sql, args, err := stmt.ToSQL()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "error generating SQL statement")
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-19 17:55:15 +00:00
|
|
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &queryR, sql, args...); err != nil {
|
2022-06-08 01:09:47 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "error executing SQL statement")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, r := range queryR {
|
2022-11-10 17:28:00 +00:00
|
|
|
result[r.HostID] = append(result[r.HostID], r.SoftwareVulnerability)
|
2022-06-08 01:09:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (ds *Datastore) ListSoftwareForVulnDetection(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
hostID uint,
|
|
|
|
|
) ([]fleet.Software, error) {
|
|
|
|
|
var result []fleet.Software
|
|
|
|
|
|
|
|
|
|
stmt := dialect.
|
|
|
|
|
From(goqu.T("software").As("s")).
|
2022-08-22 15:45:17 +00:00
|
|
|
LeftJoin(
|
2022-06-08 01:09:47 +00:00
|
|
|
goqu.T("software_cpe").As("cpe"),
|
|
|
|
|
goqu.On(goqu.Ex{
|
|
|
|
|
"s.id": goqu.I("cpe.software_id"),
|
|
|
|
|
}),
|
|
|
|
|
).
|
|
|
|
|
Join(
|
|
|
|
|
goqu.T("host_software").As("hs"),
|
|
|
|
|
goqu.On(goqu.Ex{
|
|
|
|
|
"s.id": goqu.I("hs.software_id"),
|
|
|
|
|
}),
|
|
|
|
|
).
|
|
|
|
|
Select(
|
|
|
|
|
goqu.I("s.id"),
|
|
|
|
|
goqu.I("s.name"),
|
|
|
|
|
goqu.I("s.version"),
|
2022-06-23 20:44:45 +00:00
|
|
|
goqu.I("s.release"),
|
|
|
|
|
goqu.I("s.arch"),
|
2022-08-22 15:45:17 +00:00
|
|
|
goqu.COALESCE(goqu.I("cpe.cpe"), "").As("generated_cpe"),
|
2022-06-08 01:09:47 +00:00
|
|
|
).
|
|
|
|
|
Where(goqu.C("host_id").Eq(hostID))
|
|
|
|
|
|
|
|
|
|
sql, args, err := stmt.ToSQL()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "error generating SQL statement")
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-19 17:55:15 +00:00
|
|
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &result, sql, args...); err != nil {
|
2022-06-08 01:09:47 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "error executing SQL statement")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListCVEs returns all cve_meta rows published after 'maxAge'
|
|
|
|
|
func (ds *Datastore) ListCVEs(ctx context.Context, maxAge time.Duration) ([]fleet.CVEMeta, error) {
|
|
|
|
|
var result []fleet.CVEMeta
|
|
|
|
|
|
|
|
|
|
maxAgeDate := time.Now().Add(-1 * maxAge)
|
|
|
|
|
stmt := dialect.From(goqu.T("cve_meta")).
|
|
|
|
|
Select(
|
|
|
|
|
goqu.C("cve"),
|
|
|
|
|
goqu.C("cvss_score"),
|
|
|
|
|
goqu.C("epss_probability"),
|
|
|
|
|
goqu.C("cisa_known_exploit"),
|
|
|
|
|
goqu.C("published"),
|
2023-09-15 17:24:10 +00:00
|
|
|
goqu.C("description"),
|
2022-06-08 01:09:47 +00:00
|
|
|
).
|
|
|
|
|
Where(goqu.C("published").Gte(maxAgeDate))
|
|
|
|
|
|
|
|
|
|
sql, args, err := stmt.ToSQL()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "error generating SQL statement")
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-19 17:55:15 +00:00
|
|
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &result, sql, args...); err != nil {
|
2022-06-08 01:09:47 +00:00
|
|
|
return nil, ctxerr.Wrap(ctx, err, "error executing SQL statement")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|