2021-04-26 15:44:22 +00:00
package mysql
import (
2021-09-14 12:11:07 +00:00
"context"
2025-04-09 20:08:51 +00:00
"database/sql"
2024-05-15 15:34:21 +00:00
"encoding/hex"
2025-04-09 20:08:51 +00:00
"errors"
2021-04-26 15:44:22 +00:00
"fmt"
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
"regexp"
2021-11-08 16:07:42 +00:00
"sort"
2025-04-10 22:29:15 +00:00
"strconv"
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"
2025-09-24 22:38:13 +00:00
"github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql"
2021-06-26 04:46:51 +00:00
"github.com/fleetdm/fleet/v4/server/fleet"
2024-05-15 22:39:42 +00:00
"github.com/fleetdm/fleet/v4/server/ptr"
2025-04-30 22:00:28 +00:00
"github.com/go-kit/log"
2024-06-17 13:27:31 +00:00
"github.com/go-kit/log/level"
2024-05-15 15:34:21 +00:00
"github.com/google/uuid"
2021-04-26 15:44:22 +00:00
"github.com/jmoiron/sqlx"
2025-09-24 22:38:13 +00:00
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
2021-04-26 15:44:22 +00:00
)
2025-11-07 23:33:31 +00:00
type softwareSummary struct {
2025-04-11 23:19:07 +00:00
ID uint ` db:"id" `
Checksum string ` db:"checksum" `
Name string ` db:"name" `
TitleID * uint ` db:"title_id" `
BundleIdentifier * string ` db:"bundle_identifier" `
2025-11-07 23:33:31 +00:00
UpgradeCode * string ` db:"upgrade_code" `
2025-04-11 23:19:07 +00:00
Source string ` db:"source" `
2024-05-15 15:34:21 +00:00
}
2025-09-24 22:38:13 +00:00
// tracer is an OTEL tracer. It has no-op behavior when OTEL is not enabled.
// If provider is set later (with otel.SetTracerProvider), the tracer will start using the new provider.
var tracer = otel . Tracer ( "github.com/fleetdm/fleet/v4/server/datastore/mysql" )
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 )
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
// trailingNonWordChars matches trailing anything not a letter, digit, or underscore
var trailingNonWordChars = regexp . MustCompile ( ` \W+$ ` )
2024-05-15 15:34:21 +00:00
// Since a host may have a lot of software items, we need to batch the inserts.
2024-05-24 12:26:42 +00:00
// The maximum number of software items we can insert at one time is governed by max_allowed_packet, which already be set to a high value for MDM bootstrap packages,
2024-05-15 15:34:21 +00:00
// and by the maximum number of placeholders in a prepared statement, which is 65,536. These are already fairly large limits.
// This is a variable, so it can be adjusted during unit testing.
var softwareInsertBatchSize = 1000
2025-09-24 22:38:13 +00:00
// softwareInventoryInsertBatchSize is used for pre-inserting software inventory entries
// outside the main software ingestion transaction. Smaller batches reduce lock contention.
var softwareInventoryInsertBatchSize = 100
2024-05-30 18:14:49 +00:00
func softwareSliceToMap ( softwareItems [ ] fleet . Software ) map [ string ] fleet . Software {
result := make ( map [ string ] fleet . Software , len ( softwareItems ) )
for _ , s := range softwareItems {
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 ) {
2025-09-24 22:38:13 +00:00
// OTEL instrumentation. It has no-op behavior when OTEL is not enabled.
ctx , span := tracer . Start ( ctx , "mysql.UpdateHostSoftware" ,
trace . WithSpanKind ( trace . SpanKindInternal ) ,
trace . WithAttributes (
attribute . Int ( "host_id" , int ( hostID ) ) , //nolint:gosec
attribute . Int ( "software_count" , len ( software ) ) ,
) )
defer span . End ( )
2024-05-23 19:45:50 +00:00
return ds . applyChangesForNewSoftwareDB ( ctx , hostID , software )
2023-05-17 18:49:09 +00:00
}
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
}
2025-05-05 20:47:59 +00:00
toI , toD , err := hostSoftwareInstalledPathsDelta ( hostID , reported , hsip , currS , ds . logger )
2023-05-17 18:49:09 +00:00
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 := `
2025-05-21 04:38:59 +00:00
SELECT t . id , t . host_id , t . software_id , t . installed_path , t . team_identifier , t . executable_sha256
2023-05-17 18:49:09 +00:00
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 ,
2025-05-05 20:47:59 +00:00
logger log . Logger ,
2023-05-17 18:49:09 +00:00
) (
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
}
2025-05-21 04:38:59 +00:00
var sha256 string
if r . ExecutableSHA256 != nil {
sha256 = * r . ExecutableSHA256
}
Add `team_identifier` to macOS software (#23766)
Changes to add `team_identifier` signing information to macOS
applications on the `/api/latest/fleet/hosts/:id/software` API endpoint.
Docs: https://github.com/fleetdm/fleet/pull/23743
- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [X] Added/updated tests
- [X] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [X] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [X] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [X] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ X Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [X] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
---------
Co-authored-by: Tim Lee <timlee@fleetdm.com>
Co-authored-by: Ian Littman <iansltx@gmail.com>
2024-11-15 17:17:04 +00:00
key := fmt . Sprintf (
2025-05-21 04:38:59 +00:00
"%s%s%s%s%s%s%s" ,
r . InstalledPath , fleet . SoftwareFieldSeparator , r . TeamIdentifier , fleet . SoftwareFieldSeparator , sha256 , fleet . SoftwareFieldSeparator , s . ToUniqueStr ( ) ,
Add `team_identifier` to macOS software (#23766)
Changes to add `team_identifier` signing information to macOS
applications on the `/api/latest/fleet/hosts/:id/software` API endpoint.
Docs: https://github.com/fleetdm/fleet/pull/23743
- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [X] Added/updated tests
- [X] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [X] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [X] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [X] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ X Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [X] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
---------
Co-authored-by: Tim Lee <timlee@fleetdm.com>
Co-authored-by: Ian Littman <iansltx@gmail.com>
2024-11-15 17:17:04 +00:00
)
2023-05-17 18:49:09 +00:00
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 {
2025-05-21 04:38:59 +00:00
parts := strings . SplitN ( key , fleet . SoftwareFieldSeparator , 4 )
installedPath , teamIdentifier , cdHash , unqStr := parts [ 0 ] , parts [ 1 ] , parts [ 2 ] , parts [ 3 ]
2023-05-17 18:49:09 +00:00
2025-05-05 20:47:59 +00:00
// Shouldn't be a common occurence ... everything 'reported' should be in the the software table
2023-05-17 18:49:09 +00:00
// because this executes after 'ds.UpdateHostSoftware'
s , ok := sUnqStrLook [ unqStr ]
if ! ok {
2025-05-05 20:47:59 +00:00
level . Debug ( logger ) . Log ( "msg" , "skipping installed path for software not found" , "host_id" , hostID , "unq_str" , unqStr )
continue
2023-05-17 18:49:09 +00:00
}
if _ , ok := iSPathLookup [ key ] ; ok {
// Nothing to do
continue
}
2025-05-21 04:38:59 +00:00
var executableSHA256 * string
if cdHash != "" {
executableSHA256 = ptr . String ( cdHash )
}
2023-05-17 18:49:09 +00:00
toInsert = append ( toInsert , fleet . HostSoftwareInstalledPath {
2025-05-21 04:38:59 +00:00
HostID : hostID ,
SoftwareID : s . ID ,
InstalledPath : installedPath ,
TeamIdentifier : teamIdentifier ,
ExecutableSHA256 : executableSHA256 ,
2023-05-17 18:49:09 +00:00
} )
}
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
}
2025-05-21 04:38:59 +00:00
stmt := "INSERT INTO host_software_installed_paths (host_id, software_id, installed_path, team_identifier, executable_sha256) VALUES %s"
2023-05-17 18:49:09 +00:00
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 {
2025-05-21 04:38:59 +00:00
args = append ( args , v . HostID , v . SoftwareID , v . InstalledPath , v . TeamIdentifier , v . ExecutableSHA256 )
2023-05-17 18:49:09 +00:00
}
2025-05-21 04:38:59 +00:00
placeHolders := strings . TrimSuffix ( strings . Repeat ( "(?, ?, ?, ?, ?), " , len ( batch ) ) , ", " )
2023-05-17 18:49:09 +00:00
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
}
2024-05-30 18:14:49 +00:00
func nothingChanged ( current , incoming [ ] fleet . Software , minLastOpenedAtDiff time . Duration ) (
map [ string ] fleet . Software , map [ string ] fleet . Software , bool ,
) {
// Process incoming software to ensure there are no duplicates, since the same software can be installed at multiple paths.
incomingMap := make ( map [ string ] fleet . Software , len ( current ) ) // setting len(current) as the length since that should be the common case
for _ , s := range incoming {
uniqueStr := s . ToUniqueStr ( )
if duplicate , ok := incomingMap [ uniqueStr ] ; ok {
// Check the last opened at timestamp and keep the latest.
if s . LastOpenedAt == nil ||
( duplicate . LastOpenedAt != nil && ! s . LastOpenedAt . After ( * duplicate . LastOpenedAt ) ) {
continue // keep the duplicate
}
}
incomingMap [ uniqueStr ] = s
2021-07-08 16:57:43 +00:00
}
2024-05-30 18:14:49 +00:00
currentMap := softwareSliceToMap ( current )
if len ( currentMap ) != len ( incomingMap ) {
return currentMap , incomingMap , false
2021-07-08 16:57:43 +00:00
}
2024-05-30 18:14:49 +00:00
for _ , s := range incomingMap {
2023-05-17 18:49:09 +00:00
cur , ok := currentMap [ s . ToUniqueStr ( ) ]
2022-04-26 18:16:59 +00:00
if ! ok {
2024-05-30 18:14:49 +00:00
return currentMap , incomingMap , false
2021-07-08 16:57:43 +00:00
}
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 {
2024-05-30 18:14:49 +00:00
return currentMap , incomingMap , false
2022-04-26 18:16:59 +00:00
}
oldLast := * cur . LastOpenedAt
newLast := * s . LastOpenedAt
if newLast . Sub ( oldLast ) >= minLastOpenedAtDiff {
2024-05-30 18:14:49 +00:00
return currentMap , incomingMap , false
2022-04-26 18:16:59 +00:00
}
}
2021-07-08 16:57:43 +00:00
}
2024-05-30 18:14:49 +00:00
return currentMap , incomingMap , true
2021-07-08 16:57:43 +00:00
}
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 ,
2025-10-07 21:05:22 +00:00
s . extension_for ,
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 ,
2025-11-07 23:33:31 +00:00
s . upgrade_code ,
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
}
2025-10-01 17:17:23 +00:00
// filterSoftwareWithEmptyNames removes software entries with empty names in-place.
// This is a well-known Go idiom: https://go.dev/wiki/SliceTricks#filter-in-place
func filterSoftwareWithEmptyNames ( software [ ] fleet . Software ) [ ] fleet . Software {
n := 0
for _ , sw := range software {
if sw . Name != "" {
software [ n ] = sw
n ++
}
}
return software [ : n ]
}
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
2024-05-15 15:34:21 +00:00
func ( ds * Datastore ) applyChangesForNewSoftwareDB (
2022-04-27 13:47:09 +00:00
ctx context . Context ,
hostID uint ,
2025-11-07 23:33:31 +00:00
incomingSoftware [ ] fleet . Software ,
2023-05-17 18:49:09 +00:00
) ( * fleet . UpdateHostSoftwareDBResult , error ) {
r := & fleet . UpdateHostSoftwareDBResult { }
2025-10-01 17:17:23 +00:00
// We want to make sure we have valid data before proceeding. We've seen Windows programs with empty names.
2025-11-07 23:33:31 +00:00
incomingSoftware = filterSoftwareWithEmptyNames ( incomingSoftware )
2025-10-01 17:17:23 +00:00
2025-11-07 23:33:31 +00:00
// This code executes once an hour for each host, so we should optimize for MySQL writer DB performance.
// We use a reader DB to avoid accessing the writer. If nothing has changed, we avoid all access to the writer.
// It is possible that the software list is out of sync between the reader and the writer. This is unlikely because
2024-05-15 15:34:21 +00:00
// it is updated once an hour under normal circumstances. If this does occur, the software list will be updated
// once again in an hour.
currentSoftware , err := listSoftwareByHostIDShort ( ctx , ds . reader ( ctx ) , 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
2025-11-07 23:33:31 +00:00
current , incoming , noChanges := nothingChanged ( currentSoftware , incomingSoftware , ds . minLastOpenedAtDiff )
if noChanges {
2023-05-24 19:05:45 +00:00
return r , nil
2021-07-08 16:57:43 +00:00
}
2025-11-07 23:33:31 +00:00
existingSoftwareSummaries , incomingSoftwareByChecksum , incomingChecksumsToExistingTitles , err := ds . getExistingSoftware ( ctx , current , incoming )
2024-06-12 13:38:57 +00:00
if err != nil {
return r , err
}
2025-09-24 22:38:13 +00:00
// PHASE 1: Pre-insert software inventory data outside the main transaction
// This reduces lock contention by breaking up large INSERT IGNORE operations
// into smaller, faster transactions that release locks quickly.
// These operations are idempotent due to INSERT IGNORE.
2025-11-07 23:33:31 +00:00
if len ( incomingSoftwareByChecksum ) > 0 {
2025-09-24 22:38:13 +00:00
// Pre-insert software and titles in small batches
2025-11-07 23:33:31 +00:00
err = ds . preInsertSoftwareInventory ( ctx , existingSoftwareSummaries , incomingSoftwareByChecksum , incomingChecksumsToExistingTitles )
2025-09-24 22:38:13 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "pre-insert software inventory" )
}
}
// PHASE 2: Main transaction for host-specific operations
2025-10-06 16:30:10 +00:00
err = ds . withTx (
2024-05-15 15:34:21 +00:00
ctx , func ( tx sqlx . ExtContext ) error {
deleted , err := deleteUninstalledHostSoftwareDB ( ctx , tx , hostID , current , incoming )
if err != nil {
return err
}
r . Deleted = deleted
2021-07-08 16:57:43 +00:00
2025-09-24 22:38:13 +00:00
// Link the pre-inserted software to this host
// Software inventory entries were already created in Phase 1
2025-11-07 23:33:31 +00:00
inserted , err := ds . linkSoftwareToHost ( ctx , tx , hostID , incomingSoftwareByChecksum )
2024-05-15 15:34:21 +00:00
if err != nil {
return err
}
r . Inserted = inserted
2022-04-26 18:16:59 +00:00
2025-09-24 22:38:13 +00:00
if err = checkForDeletedInstalledSoftware ( ctx , tx , deleted , r . Inserted , hostID ) ; err != nil {
2024-08-26 22:30:56 +00:00
return err
}
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
if err = updateModifiedHostSoftwareDB ( ctx , tx , hostID , current , incoming , ds . minLastOpenedAtDiff , ds . logger ) ; err != nil {
2025-04-11 23:19:07 +00:00
return err
}
2024-05-15 15:34:21 +00:00
if err = updateSoftwareUpdatedAt ( ctx , tx , hostID ) ; err != nil {
return err
}
return nil
} ,
)
if err != nil {
2023-05-17 18:49:09 +00:00
return nil , err
2023-01-09 11:55:43 +00:00
}
2024-05-15 15:34:21 +00:00
return r , err
2021-07-08 16:57:43 +00:00
}
2024-08-26 22:30:56 +00:00
func checkForDeletedInstalledSoftware ( ctx context . Context , tx sqlx . ExtContext , deleted [ ] fleet . Software , inserted [ ] fleet . Software ,
2024-09-13 14:28:26 +00:00
hostID uint ,
) error {
2024-08-26 22:30:56 +00:00
// Between deleted and inserted software, check which software titles were deleted.
// If software titles were deleted, get the software titles of the installed software.
// See if deleted titles match installed software titles.
// If so, mark the installed software as removed.
var deletedTitles map [ string ] struct { }
if len ( deleted ) > 0 {
deletedTitles = make ( map [ string ] struct { } , len ( deleted ) )
for _ , d := range deleted {
// We don't support installing browser plugins as of 2024/08/22
2025-10-07 21:05:22 +00:00
if d . ExtensionFor == "" {
2025-02-03 19:23:21 +00:00
deletedTitles [ UniqueSoftwareTitleStr ( BundleIdentifierOrName ( d . BundleIdentifier , d . Name ) , d . Source ) ] = struct { } { }
2024-08-26 22:30:56 +00:00
}
}
for _ , i := range inserted {
// We don't support installing browser plugins as of 2024/08/22
2025-10-07 21:05:22 +00:00
if i . ExtensionFor == "" {
2025-09-02 19:05:42 +00:00
key := UniqueSoftwareTitleStr ( BundleIdentifierOrName ( i . BundleIdentifier , i . Name ) , i . Source )
2024-10-29 19:17:51 +00:00
delete ( deletedTitles , key )
2024-08-26 22:30:56 +00:00
}
}
}
if len ( deletedTitles ) > 0 {
installedTitles , err := getInstalledByFleetSoftwareTitles ( ctx , tx , hostID )
if err != nil {
return err
}
type deletedValue struct {
vpp bool
}
deletedTitleIDs := make ( map [ uint ] deletedValue , 0 )
for _ , title := range installedTitles {
bundleIdentifier := ""
if title . BundleIdentifier != nil {
bundleIdentifier = * title . BundleIdentifier
}
2025-02-03 19:23:21 +00:00
key := UniqueSoftwareTitleStr ( BundleIdentifierOrName ( bundleIdentifier , title . Name ) , title . Source )
2024-08-26 22:30:56 +00:00
if _ , ok := deletedTitles [ key ] ; ok {
deletedTitleIDs [ title . ID ] = deletedValue { vpp : title . VPPAppsCount > 0 }
}
}
if len ( deletedTitleIDs ) > 0 {
IDs := make ( [ ] uint , 0 , len ( deletedTitleIDs ) )
vppIDs := make ( [ ] uint , 0 , len ( deletedTitleIDs ) )
for id , value := range deletedTitleIDs {
if value . vpp {
vppIDs = append ( vppIDs , id )
} else {
IDs = append ( IDs , id )
}
}
if len ( IDs ) > 0 {
if err = markHostSoftwareInstallsRemoved ( ctx , tx , hostID , IDs ) ; err != nil {
return err
}
}
if len ( vppIDs ) > 0 {
if err = markHostVPPSoftwareInstallsRemoved ( ctx , tx , hostID , vppIDs ) ; err != nil {
return err
}
}
}
}
return nil
}
2024-06-12 13:38:57 +00:00
func ( ds * Datastore ) getExistingSoftware (
ctx context . Context , current map [ string ] fleet . Software , incoming map [ string ] fleet . Software ,
) (
2025-11-07 23:33:31 +00:00
currentSoftwareSummaries [ ] softwareSummary ,
newChecksumsToSoftware map [ string ] fleet . Software ,
incomingChecksumsToTitles map [ string ] fleet . SoftwareTitle ,
2025-04-11 23:19:07 +00:00
err error ,
2024-06-12 13:38:57 +00:00
) {
2025-11-07 23:33:31 +00:00
// TODO(jacob) - the `incoming` argument here should already contain a map of checksum:Software, put
// together by the `nothingChanged` function upstream. Is this redundant?
2024-06-12 13:38:57 +00:00
// Compute checksums for all incoming software, which we will use for faster retrieval, since checksum is a unique index
2025-11-07 23:33:31 +00:00
newChecksumsToSoftware = make ( map [ string ] fleet . Software , len ( current ) )
// TODO(jacob) - below set seems to be the same as above map but without Software values for each key.
// Are both necessary, or can we just use the map everywhere?
setOfNewSWChecksums := make ( map [ string ] struct { } )
2024-06-12 13:38:57 +00:00
for uniqueName , s := range incoming {
2025-04-11 23:19:07 +00:00
_ , ok := current [ uniqueName ]
if ! ok {
2025-11-07 23:33:31 +00:00
// -> incoming SW is new
2025-04-11 23:19:07 +00:00
checksum , err := s . ComputeRawChecksum ( )
2024-06-12 13:38:57 +00:00
if err != nil {
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
return nil , nil , nil , err
2024-06-12 13:38:57 +00:00
}
2025-11-07 23:33:31 +00:00
newChecksumsToSoftware [ string ( checksum ) ] = s
setOfNewSWChecksums [ string ( checksum ) ] = struct { } { }
2024-06-12 13:38:57 +00:00
}
}
2025-11-07 23:33:31 +00:00
if len ( newChecksumsToSoftware ) > 0 {
sliceOfNewSWChecksums := make ( [ ] string , 0 , len ( newChecksumsToSoftware ) )
for checksum := range newChecksumsToSoftware {
sliceOfNewSWChecksums = append ( sliceOfNewSWChecksums , checksum )
2024-06-12 13:38:57 +00:00
}
2025-11-07 23:33:31 +00:00
// We use the replica DB for retrieval to minimize the traffic to the writer DB.
// It is OK if the software is not found in the replica DB, because we will then attempt to insert it in the writer DB.
currentSoftwareSummaries , err = getExistingSoftwareSummariesByChecksums ( ctx , ds . reader ( ctx ) , sliceOfNewSWChecksums )
2024-06-12 13:38:57 +00:00
if err != nil {
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
return nil , nil , nil , err
2024-06-12 13:38:57 +00:00
}
2025-10-06 16:32:26 +00:00
2025-11-07 23:33:31 +00:00
for _ , currentSoftwareSummary := range currentSoftwareSummaries {
_ , ok := newChecksumsToSoftware [ currentSoftwareSummary . Checksum ]
2024-06-12 13:38:57 +00:00
if ! ok {
// This should never happen. If it does, we have a bug.
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
return nil , nil , nil , ctxerr . New (
2025-11-07 23:33:31 +00:00
ctx , fmt . Sprintf ( "current software: software not found for checksum %s" , hex . EncodeToString ( [ ] byte ( currentSoftwareSummary . Checksum ) ) ) ,
2024-06-12 13:38:57 +00:00
)
}
2025-11-07 23:33:31 +00:00
delete ( setOfNewSWChecksums , currentSoftwareSummary . Checksum )
2024-06-12 13:38:57 +00:00
}
}
2025-11-07 23:33:31 +00:00
if len ( setOfNewSWChecksums ) == 0 {
return currentSoftwareSummaries , newChecksumsToSoftware , incomingChecksumsToTitles , nil
2025-01-23 18:48:21 +00:00
}
// There's new software, so we try to get the titles already stored in `software_titles` for them.
2025-11-07 23:33:31 +00:00
incomingChecksumsToTitles , _ , err = ds . getIncomingSoftwareChecksumsToExistingTitles ( ctx , setOfNewSWChecksums , newChecksumsToSoftware )
2025-01-23 18:48:21 +00:00
if err != nil {
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
return nil , nil , nil , ctxerr . Wrap ( ctx , err , "get incoming software checksums to existing titles" )
2025-01-23 18:48:21 +00:00
}
2025-11-07 23:33:31 +00:00
return currentSoftwareSummaries , newChecksumsToSoftware , incomingChecksumsToTitles , nil
2025-01-23 18:48:21 +00:00
}
// getIncomingSoftwareChecksumsToExistingTitles loads the existing titles for the new incoming software.
// It returns a map of software checksums to existing software titles.
//
// To make best use of separate indexes, it runs two queries to get the existing titles from the DB:
// - One query for software with bundle_identifier.
// - One query for software without bundle_identifier.
2025-11-07 23:33:31 +00:00
//
// TODO(jacob) - consider index and appropriate query here for Windows software `upgrade_code`s, similar to
// bundle identifier, if needed for optimization
2025-01-23 18:48:21 +00:00
func ( ds * Datastore ) getIncomingSoftwareChecksumsToExistingTitles (
ctx context . Context ,
newSoftwareChecksums map [ string ] struct { } ,
incomingChecksumToSoftware map [ string ] fleet . Software ,
2025-04-11 23:19:07 +00:00
) ( map [ string ] fleet . SoftwareTitle , map [ string ] fleet . Software , error ) {
2025-01-23 18:48:21 +00:00
var (
2025-11-07 23:33:31 +00:00
incomingChecksumsToTitles = make ( map [ string ] fleet . SoftwareTitle , len ( newSoftwareChecksums ) )
2025-04-11 23:19:07 +00:00
argsWithoutBundleIdentifier [ ] any
argsWithBundleIdentifier [ ] any
2025-09-24 22:38:13 +00:00
uniqueTitleStrToChecksums = make ( map [ string ] [ ] string )
2025-01-23 18:48:21 +00:00
)
2025-04-11 23:19:07 +00:00
bundleIDsToIncomingNames := make ( map [ string ] string )
2025-01-23 18:48:21 +00:00
for checksum := range newSoftwareChecksums {
sw := incomingChecksumToSoftware [ checksum ]
if sw . BundleIdentifier != "" {
2025-04-11 23:19:07 +00:00
bundleIDsToIncomingNames [ sw . BundleIdentifier ] = sw . Name
2025-01-23 18:48:21 +00:00
argsWithBundleIdentifier = append ( argsWithBundleIdentifier , sw . BundleIdentifier )
} else {
2025-11-07 23:33:31 +00:00
// TODO(jacob) - consider `upgrade_code` here and below if needed for additional specificity
2025-10-07 21:05:22 +00:00
argsWithoutBundleIdentifier = append ( argsWithoutBundleIdentifier , sw . Name , sw . Source , sw . ExtensionFor )
2025-01-23 18:48:21 +00:00
}
// Map software title identifier to software checksums so that we can map checksums to actual titles later.
2025-09-24 22:38:13 +00:00
// Note: Multiple checksums can map to the same title (e.g., when names are truncated). This should not normally happen.
titleStr := UniqueSoftwareTitleStr (
2025-10-07 21:05:22 +00:00
BundleIdentifierOrName ( sw . BundleIdentifier , sw . Name ) , sw . Source , sw . ExtensionFor ,
2025-09-24 22:38:13 +00:00
)
existingChecksums := uniqueTitleStrToChecksums [ titleStr ]
if len ( existingChecksums ) > 0 {
2025-09-30 14:35:47 +00:00
// Log when multiple checksums map to the same title.
2025-10-01 17:17:23 +00:00
existingChecksumsHex := make ( [ ] string , len ( existingChecksums ) )
for i , cs := range existingChecksums {
existingChecksumsHex [ i ] = fmt . Sprintf ( "%x" , cs )
}
2025-09-30 14:35:47 +00:00
level . Debug ( ds . logger ) . Log (
2025-09-24 22:38:13 +00:00
"msg" , "multiple checksums mapping to same title" ,
"title_str" , titleStr ,
2025-10-01 17:17:23 +00:00
"new_checksum" , fmt . Sprintf ( "%x" , checksum ) ,
"existing_checksums" , fmt . Sprintf ( "%v" , existingChecksumsHex ) ,
2025-09-24 22:38:13 +00:00
"software_name" , sw . Name ,
"software_version" , sw . Version ,
)
}
uniqueTitleStrToChecksums [ titleStr ] = append ( uniqueTitleStrToChecksums [ titleStr ] , checksum )
2025-01-23 18:48:21 +00:00
}
// Get titles for software without bundle_identifier.
if len ( argsWithoutBundleIdentifier ) > 0 {
2025-09-19 20:35:05 +00:00
// Build IN clause with composite values for better performance
2025-10-07 21:05:22 +00:00
// Each triplet of args represents (name, source, extension_for)
2025-09-19 20:35:05 +00:00
numItems := len ( argsWithoutBundleIdentifier ) / 3
valuePlaceholders := make ( [ ] string , 0 , numItems )
for i := 0 ; i < numItems ; i ++ {
valuePlaceholders = append ( valuePlaceholders , "(?, ?, ?)" )
}
2024-06-12 13:38:57 +00:00
stmt := fmt . Sprintf (
2025-10-07 21:05:22 +00:00
"SELECT id, name, source, extension_for FROM software_titles WHERE (name, source, extension_for) IN (%s)" ,
2025-09-19 20:35:05 +00:00
strings . Join ( valuePlaceholders , ", " ) ,
2024-06-12 13:38:57 +00:00
)
2025-01-23 18:48:21 +00:00
var existingSoftwareTitlesForNewSoftwareWithoutBundleIdentifier [ ] fleet . SoftwareTitle
if err := sqlx . SelectContext ( ctx ,
ds . reader ( ctx ) ,
& existingSoftwareTitlesForNewSoftwareWithoutBundleIdentifier ,
stmt ,
argsWithoutBundleIdentifier ... ,
) ; err != nil {
2025-04-11 23:19:07 +00:00
return nil , nil , ctxerr . Wrap ( ctx , err , "get existing titles without bundle identifier" )
2024-06-12 13:38:57 +00:00
}
2025-01-23 18:48:21 +00:00
for _ , title := range existingSoftwareTitlesForNewSoftwareWithoutBundleIdentifier {
2025-10-07 21:05:22 +00:00
checksums , ok := uniqueTitleStrToChecksums [ UniqueSoftwareTitleStr ( title . Name , title . Source , title . ExtensionFor ) ]
2025-01-23 18:48:21 +00:00
if ok {
2025-09-24 22:38:13 +00:00
// Map all checksums that correspond to this title
for _ , checksum := range checksums {
2025-11-07 23:33:31 +00:00
incomingChecksumsToTitles [ checksum ] = title
2025-09-24 22:38:13 +00:00
}
2025-01-23 18:48:21 +00:00
}
2024-06-12 13:38:57 +00:00
}
2025-01-23 18:48:21 +00:00
}
2024-06-12 13:38:57 +00:00
2025-01-23 18:48:21 +00:00
// Get titles for software with bundle_identifier
2025-04-11 23:19:07 +00:00
existingBundleIDsToUpdate := make ( map [ string ] fleet . Software )
2025-01-23 18:48:21 +00:00
if len ( argsWithBundleIdentifier ) > 0 {
2025-04-11 23:19:07 +00:00
// no-op code change
2025-11-07 23:33:31 +00:00
// TODO(jacob) - this var name is shadowing the one in the outer scope. Is this successfully
// adding titles-by-checksum for software with bundle ids?
incomingChecksumsToTitles = make ( map [ string ] fleet . SoftwareTitle , len ( newSoftwareChecksums ) )
2025-10-07 21:05:22 +00:00
stmtBundleIdentifier := ` SELECT id, name, source, extension_for, bundle_identifier FROM software_titles WHERE bundle_identifier IN (?) `
2025-01-23 18:48:21 +00:00
stmtBundleIdentifier , argsWithBundleIdentifier , err := sqlx . In ( stmtBundleIdentifier , argsWithBundleIdentifier )
if err != nil {
2025-11-07 23:33:31 +00:00
return nil , nil , ctxerr . Wrap ( ctx , err , "build query to get existing titles with bundle_identifier" )
2025-01-23 18:48:21 +00:00
}
var existingSoftwareTitlesForNewSoftwareWithBundleIdentifier [ ] fleet . SoftwareTitle
if err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & existingSoftwareTitlesForNewSoftwareWithBundleIdentifier , stmtBundleIdentifier , argsWithBundleIdentifier ... ) ; err != nil {
2025-04-11 23:19:07 +00:00
return nil , nil , ctxerr . Wrap ( ctx , err , "get existing titles with bundle_identifier" )
2025-01-23 18:48:21 +00:00
}
2024-06-12 13:38:57 +00:00
// Map software titles to software checksums.
2025-01-23 18:48:21 +00:00
for _ , title := range existingSoftwareTitlesForNewSoftwareWithBundleIdentifier {
2025-10-07 21:05:22 +00:00
uniqueStrWithoutName := UniqueSoftwareTitleStr ( * title . BundleIdentifier , title . Source , title . ExtensionFor )
2025-09-24 22:38:13 +00:00
checksums , withoutName := uniqueTitleStrToChecksums [ uniqueStrWithoutName ]
2025-04-11 23:19:07 +00:00
if withoutName {
2025-09-24 22:38:13 +00:00
// Map all checksums that correspond to this title
for _ , checksum := range checksums {
2025-11-07 23:33:31 +00:00
incomingChecksumsToTitles [ checksum ] = title
2025-09-24 22:38:13 +00:00
}
2024-06-12 13:38:57 +00:00
}
}
}
2025-11-07 23:33:31 +00:00
return incomingChecksumsToTitles , existingBundleIDsToUpdate , nil
2024-06-12 13:38:57 +00:00
}
2025-02-03 19:23:21 +00:00
// BundleIdentifierOrName returns the bundle identifier if it is not empty, otherwise name
func BundleIdentifierOrName ( bundleIdentifier , name string ) string {
if bundleIdentifier != "" {
return bundleIdentifier
}
return name
}
2024-06-12 13:38:57 +00:00
// UniqueSoftwareTitleStr creates a unique string representation of the software title
func UniqueSoftwareTitleStr ( values ... string ) string {
return strings . Join ( values , fleet . SoftwareFieldSeparator )
}
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
}
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
// longestCommonPrefix finds the longest common prefix among a slice of strings.
// Returns empty string if there's no common prefix.
func longestCommonPrefix ( strs [ ] string ) string {
if len ( strs ) == 0 {
return ""
}
if len ( strs ) == 1 {
return strs [ 0 ]
}
firstLen := len ( strs [ 0 ] )
i := 0
for {
if i >= firstLen {
return strs [ 0 ]
}
c := strs [ 0 ] [ i ]
for _ , s := range strs [ 1 : ] {
if i >= len ( s ) || s [ i ] != c {
return strs [ 0 ] [ : i ]
}
}
i ++
}
}
2025-09-24 22:38:13 +00:00
// preInsertSoftwareInventory pre-inserts software and software_titles outside the main transaction
// to reduce lock contention. These operations are idempotent due to INSERT IGNORE.
func ( ds * Datastore ) preInsertSoftwareInventory (
2021-09-14 14:44:02 +00:00
ctx context . Context ,
2025-11-07 23:33:31 +00:00
existingSoftwareSummaries [ ] softwareSummary ,
incomingSoftwareByChecksum map [ string ] fleet . Software ,
incomingChecksumsToExistingTitles map [ string ] fleet . SoftwareTitle ,
2025-09-24 22:38:13 +00:00
) error {
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
type titleKey struct {
name string
source string
extensionFor string
bundleID string
isKernel bool
}
2025-09-24 22:38:13 +00:00
// Collect all software that needs to be inserted
needsInsert := make ( map [ string ] fleet . Software )
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
bundleGroups := make ( map [ titleKey ] [ ] string )
2025-11-07 23:33:31 +00:00
keys := make ( [ ] string , 0 , len ( incomingSoftwareByChecksum ) )
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
2025-11-07 23:33:31 +00:00
existingSet := make ( map [ string ] struct { } , len ( existingSoftwareSummaries ) )
for _ , es := range existingSoftwareSummaries {
2025-09-24 22:38:13 +00:00
existingSet [ es . Checksum ] = struct { } { }
}
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
2025-11-07 23:33:31 +00:00
for checksum , sw := range incomingSoftwareByChecksum {
2025-09-24 22:38:13 +00:00
if _ , ok := existingSet [ checksum ] ; ! ok {
needsInsert [ checksum ] = sw
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
keys = append ( keys , checksum )
if sw . BundleIdentifier != "" {
key := titleKey {
bundleID : sw . BundleIdentifier ,
source : sw . Source ,
extensionFor : sw . ExtensionFor ,
}
bundleGroups [ key ] = append ( bundleGroups [ key ] , sw . Name )
}
2025-04-11 23:19:07 +00:00
}
}
2022-04-26 18:16:59 +00:00
2025-09-24 22:38:13 +00:00
if len ( needsInsert ) == 0 {
return nil
}
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
bestTitleNames := make ( map [ titleKey ] string )
for key , names := range bundleGroups {
if len ( names ) > 1 {
// Pick the best represenative name for the group of names
commonPrefix := longestCommonPrefix ( names )
commonPrefix = trailingNonWordChars . ReplaceAllString ( commonPrefix , "" )
if len ( commonPrefix ) > 0 {
bestTitleNames [ key ] = commonPrefix
} else {
// Fall back to shortest name
shortest := names [ 0 ]
for _ , name := range names [ 1 : ] {
if len ( name ) < len ( shortest ) {
shortest = name
}
}
bestTitleNames [ key ] = shortest
}
} else if len ( names ) == 1 {
// Single title or no bundle_identifier
bestTitleNames [ key ] = names [ 0 ]
}
2025-09-24 22:38:13 +00:00
}
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
// Process in smaller batches to reduce lock time
2025-09-24 22:38:13 +00:00
err := common_mysql . BatchProcessSimple ( keys , softwareInventoryInsertBatchSize , func ( batchKeys [ ] string ) error {
batchSoftware := make ( map [ string ] fleet . Software , len ( batchKeys ) )
for _ , key := range batchKeys {
batchSoftware [ key ] = needsInsert [ key ]
}
// Each batch in its own transaction
return ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
// First insert any needed software titles
newTitlesNeeded := make ( map [ string ] fleet . SoftwareTitle )
for checksum , sw := range batchSoftware {
2025-11-07 23:33:31 +00:00
if _ , ok := incomingChecksumsToExistingTitles [ checksum ] ; ! ok {
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
titleName := sw . Name
if sw . BundleIdentifier != "" {
key := titleKey {
bundleID : sw . BundleIdentifier ,
source : sw . Source ,
extensionFor : sw . ExtensionFor ,
}
if computedName , exists := bestTitleNames [ key ] ; exists {
titleName = computedName
}
}
2025-09-24 22:38:13 +00:00
st := fleet . SoftwareTitle {
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
Name : titleName ,
2025-10-07 21:05:22 +00:00
Source : sw . Source ,
ExtensionFor : sw . ExtensionFor ,
IsKernel : sw . IsKernel ,
2025-09-24 22:38:13 +00:00
}
if sw . BundleIdentifier != "" {
st . BundleIdentifier = ptr . String ( sw . BundleIdentifier )
2025-04-16 20:17:59 +00:00
}
2025-10-08 14:24:38 +00:00
if sw . ApplicationID != nil && * sw . ApplicationID != "" {
st . ApplicationID = sw . ApplicationID
}
2025-11-07 23:33:31 +00:00
if sw . UpgradeCode != nil {
// intentionally write both empty and non-empty strings as upgrade codes
st . UpgradeCode = sw . UpgradeCode
}
2025-09-24 22:38:13 +00:00
newTitlesNeeded [ checksum ] = st
2025-04-16 20:17:59 +00:00
}
2024-05-15 15:34:21 +00:00
}
2025-09-24 22:38:13 +00:00
// Map to store title IDs for all titles (both existing and new)
2025-11-07 23:33:31 +00:00
titleIDsByChecksum := make ( map [ string ] uint , len ( incomingChecksumsToExistingTitles ) )
2025-09-24 22:38:13 +00:00
// First, add existing titles to the map
2025-11-07 23:33:31 +00:00
for checksum , title := range incomingChecksumsToExistingTitles {
2025-09-24 22:38:13 +00:00
titleIDsByChecksum [ checksum ] = title . ID
}
2025-11-07 23:33:31 +00:00
// TODO: somewhere around here: if new SW title has diff upgrade_code from existing, log as an error and
// do NOT insert the new title
2025-09-24 22:38:13 +00:00
if len ( newTitlesNeeded ) > 0 {
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
uniqueTitlesToInsert := make ( map [ titleKey ] fleet . SoftwareTitle )
2025-09-24 22:38:13 +00:00
for _ , title := range newTitlesNeeded {
bundleID := ""
if title . BundleIdentifier != nil {
bundleID = * title . BundleIdentifier
}
key := titleKey {
2025-10-07 21:05:22 +00:00
name : title . Name ,
source : title . Source ,
extensionFor : title . ExtensionFor ,
bundleID : bundleID ,
isKernel : title . IsKernel ,
2025-09-24 22:38:13 +00:00
}
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
if _ , exists := uniqueTitlesToInsert [ key ] ; ! exists {
uniqueTitlesToInsert [ key ] = title
}
2025-09-24 22:38:13 +00:00
}
// Insert software titles
2025-11-07 23:33:31 +00:00
const numberOfArgsPerSoftwareTitles = 7
titlesValues := strings . TrimSuffix ( strings . Repeat ( "(?,?,?,?,?,?,?)," , len ( uniqueTitlesToInsert ) ) , "," )
titlesStmt := fmt . Sprintf ( "INSERT IGNORE INTO software_titles (name, source, extension_for, bundle_identifier, is_kernel, application_id, upgrade_code) VALUES %s" , titlesValues )
2025-09-24 22:38:13 +00:00
titlesArgs := make ( [ ] any , 0 , len ( uniqueTitlesToInsert ) * numberOfArgsPerSoftwareTitles )
for _ , title := range uniqueTitlesToInsert {
2025-11-07 23:33:31 +00:00
titlesArgs = append ( titlesArgs , title . Name , title . Source , title . ExtensionFor , title . BundleIdentifier , title . IsKernel , title . ApplicationID , title . UpgradeCode )
2025-09-24 22:38:13 +00:00
}
if _ , err := tx . ExecContext ( ctx , titlesStmt , titlesArgs ... ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "pre-insert software_titles" )
}
2025-11-07 23:33:31 +00:00
// TODO(jacob) - incorporate UpgradeCode here?
2025-09-24 22:38:13 +00:00
// Retrieve the IDs for the titles we just inserted (or that already existed)
var titlesData [ ] struct {
ID uint ` db:"id" `
Name string ` db:"name" `
Source string ` db:"source" `
2025-10-07 21:05:22 +00:00
ExtensionFor string ` db:"extension_for" `
2025-09-24 22:38:13 +00:00
BundleIdentifier * string ` db:"bundle_identifier" `
}
titlePlaceholders := strings . TrimSuffix ( strings . Repeat ( "(?,?,?,?)," , len ( uniqueTitlesToInsert ) ) , "," )
queryArgs := make ( [ ] interface { } , 0 , len ( uniqueTitlesToInsert ) * 4 )
for tk := range uniqueTitlesToInsert {
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
title := uniqueTitlesToInsert [ tk ]
2025-09-24 22:38:13 +00:00
bundleID := ""
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
if title . BundleIdentifier != nil {
bundleID = * title . BundleIdentifier
2025-09-24 22:38:13 +00:00
}
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
firstArg := title . Name
if bundleID != "" {
firstArg = bundleID
}
queryArgs = append ( queryArgs , firstArg , title . Source , title . ExtensionFor , bundleID )
2025-09-24 22:38:13 +00:00
}
2025-10-07 21:05:22 +00:00
queryTitles := fmt . Sprintf ( ` SELECT id , name , source , extension_for , bundle_identifier
2025-09-24 22:38:13 +00:00
FROM software_titles
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
WHERE ( COALESCE ( bundle_identifier , name ) , source , extension_for , COALESCE ( bundle_identifier , ' ' ) ) IN ( % s ) ` , titlePlaceholders )
2025-09-24 22:38:13 +00:00
if err := sqlx . SelectContext ( ctx , tx , & titlesData , queryTitles , queryArgs ... ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "select software titles" )
}
// Map the titles back to their checksums
for _ , td := range titlesData {
var bundleID string
if td . BundleIdentifier != nil {
bundleID = * td . BundleIdentifier
}
for checksum , title := range newTitlesNeeded {
var titleBundleID string
if title . BundleIdentifier != nil {
titleBundleID = * title . BundleIdentifier
}
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
// For apps with bundle_identifier, match by bundle_identifier (since we may have picked a different name)
// For others, match by name
nameMatches := td . Name == title . Name
if bundleID != "" && titleBundleID != "" {
// Both have bundle_identifier - match by bundle_identifier instead of name
nameMatches = true
}
if nameMatches && td . Source == title . Source && td . ExtensionFor == title . ExtensionFor && bundleID == titleBundleID {
2025-09-24 22:38:13 +00:00
titleIDsByChecksum [ checksum ] = td . ID
// Don't break here - multiple checksums can map to the same title
// (e.g., when software has same truncated name but different versions (very rare))
}
}
}
2024-05-15 15:34:21 +00:00
}
2024-06-12 13:38:57 +00:00
2025-09-24 22:38:13 +00:00
// Insert software entries
2025-11-07 23:33:31 +00:00
const numberOfArgsPerSoftware = 13
2024-05-15 15:34:21 +00:00
values := strings . TrimSuffix (
2025-11-07 23:33:31 +00:00
strings . Repeat ( "(?,?,?,?,?,?,?,?,?,?,?,?,?)," , len ( batchKeys ) ) , "," ,
2024-05-15 15:34:21 +00:00
)
stmt := fmt . Sprintf (
Add `team_identifier` to macOS software (#23766)
Changes to add `team_identifier` signing information to macOS
applications on the `/api/latest/fleet/hosts/:id/software` API endpoint.
Docs: https://github.com/fleetdm/fleet/pull/23743
- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [X] Added/updated tests
- [X] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [X] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [X] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [X] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ X Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [X] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
---------
Co-authored-by: Tim Lee <timlee@fleetdm.com>
Co-authored-by: Ian Littman <iansltx@gmail.com>
2024-11-15 17:17:04 +00:00
` INSERT IGNORE INTO software (
name ,
version ,
source ,
` +" ` release ` "+ ` ,
vendor ,
arch ,
bundle_identifier ,
extension_id ,
2025-10-07 21:05:22 +00:00
extension_for ,
Add `team_identifier` to macOS software (#23766)
Changes to add `team_identifier` signing information to macOS
applications on the `/api/latest/fleet/hosts/:id/software` API endpoint.
Docs: https://github.com/fleetdm/fleet/pull/23743
- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [X] Added/updated tests
- [X] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [X] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [X] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [X] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ X Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [X] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
---------
Co-authored-by: Tim Lee <timlee@fleetdm.com>
Co-authored-by: Ian Littman <iansltx@gmail.com>
2024-11-15 17:17:04 +00:00
title_id ,
2025-10-08 14:24:38 +00:00
checksum ,
2025-11-07 23:33:31 +00:00
application_id ,
upgrade_code
Add `team_identifier` to macOS software (#23766)
Changes to add `team_identifier` signing information to macOS
applications on the `/api/latest/fleet/hosts/:id/software` API endpoint.
Docs: https://github.com/fleetdm/fleet/pull/23743
- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [X] Added/updated tests
- [X] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [X] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [X] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [X] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ X Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [X] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
---------
Co-authored-by: Tim Lee <timlee@fleetdm.com>
Co-authored-by: Ian Littman <iansltx@gmail.com>
2024-11-15 17:17:04 +00:00
) VALUES % s ` ,
2024-05-15 15:34:21 +00:00
values ,
)
2024-07-09 16:43:21 +00:00
2025-09-24 22:38:13 +00:00
args := make ( [ ] any , 0 , len ( batchKeys ) * numberOfArgsPerSoftware )
var missingSoftwareTitles [ ] string
for _ , checksum := range batchKeys {
sw := batchSoftware [ checksum ]
var titleID * uint
// Get the title ID from our combined map
if id , ok := titleIDsByChecksum [ checksum ] ; ok {
titleID = & id
} else {
// Track software missing title IDs for debugging
missingSoftwareTitles = append ( missingSoftwareTitles ,
fmt . Sprintf ( "%s %s %s" , sw . Name , sw . Version , sw . Source ) )
2024-06-12 13:38:57 +00:00
}
2024-05-15 15:34:21 +00:00
args = append (
2025-09-24 22:38:13 +00:00
args , sw . Name , sw . Version , sw . Source , sw . Release , sw . Vendor , sw . Arch ,
2025-11-07 23:33:31 +00:00
sw . BundleIdentifier , sw . ExtensionID , sw . ExtensionFor , titleID , checksum , sw . ApplicationID , sw . UpgradeCode ,
2025-09-24 22:38:13 +00:00
)
}
// Log an error if we have software without title IDs
// This shouldn't happen in normal operation. And this code is here to catch bugs.
if len ( missingSoftwareTitles ) > 0 && ds . logger != nil {
exampleCount := 3
if len ( missingSoftwareTitles ) < exampleCount {
exampleCount = len ( missingSoftwareTitles )
}
level . Error ( ds . logger ) . Log (
"msg" , "inserting software without title_id" ,
"count" , len ( missingSoftwareTitles ) ,
"examples" , strings . Join ( missingSoftwareTitles [ : exampleCount ] , "; " ) ,
2024-05-15 15:34:21 +00:00
)
}
2025-09-24 22:38:13 +00:00
2024-05-15 15:34:21 +00:00
if _ , err := tx . ExecContext ( ctx , stmt , args ... ) ; err != nil {
2025-09-24 22:38:13 +00:00
return ctxerr . Wrap ( ctx , err , "pre-insert software" )
2024-05-15 15:34:21 +00:00
}
2024-06-12 13:38:57 +00:00
2025-09-24 22:38:13 +00:00
return nil
} )
} )
2024-06-12 13:38:57 +00:00
2025-09-24 22:38:13 +00:00
return err
}
2024-07-11 17:33:20 +00:00
2025-09-24 22:38:13 +00:00
// linkSoftwareToHost links pre-inserted software to a host.
// This assumes software inventory entries already exist.
func ( ds * Datastore ) linkSoftwareToHost (
ctx context . Context ,
tx sqlx . ExtContext ,
hostID uint ,
softwareChecksums map [ string ] fleet . Software ,
) ( [ ] fleet . Software , error ) {
var insertsHostSoftware [ ] interface { }
var insertedSoftware [ ] fleet . Software
// Build map of all checksums we need to link
allChecksums := make ( [ ] string , 0 , len ( softwareChecksums ) )
for checksum := range softwareChecksums {
allChecksums = append ( allChecksums , checksum )
}
// Get all software IDs (they should exist from pre-insertion).
// This ensures that we're not creating orphaned references (where software was deleted between pre-insertion and now).
// This DB call could be removed to squeeze our a little more performance at the risk of orphaned references.
2025-11-07 23:33:31 +00:00
allSoftwareSummaries , err := getExistingSoftwareSummariesByChecksums ( ctx , tx , allChecksums )
2025-09-24 22:38:13 +00:00
if err != nil {
return nil , err
}
// Build ID map
2025-11-07 23:33:31 +00:00
softwareSummaryByChecksum := make ( map [ string ] softwareSummary )
for _ , s := range allSoftwareSummaries {
softwareSummaryByChecksum [ s . Checksum ] = s
2025-09-24 22:38:13 +00:00
}
// Link software to host
for checksum , sw := range softwareChecksums {
2025-11-07 23:33:31 +00:00
if existing , ok := softwareSummaryByChecksum [ checksum ] ; ok {
2025-09-24 22:38:13 +00:00
sw . ID = existing . ID
insertsHostSoftware = append ( insertsHostSoftware , hostID , sw . ID , sw . LastOpenedAt )
insertedSoftware = append ( insertedSoftware , sw )
} else {
// Log missing software but continue
level . Warn ( ds . logger ) . Log (
"msg" , "software not found after pre-insertion" ,
2025-10-01 17:17:23 +00:00
"checksum" , fmt . Sprintf ( "%x" , checksum ) ,
2025-09-24 22:38:13 +00:00
"name" , sw . Name ,
"version" , sw . Version ,
2024-05-15 15:34:21 +00:00
)
2021-07-08 16:57:43 +00:00
}
}
2022-04-26 18:16:59 +00:00
2025-09-24 22:38:13 +00:00
// Insert host_software links
// INSERT IGNORE handles duplicate key errors for idempotency.
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 ) , "," )
2025-09-24 22:38:13 +00:00
stmt := fmt . Sprintf ( ` INSERT IGNORE INTO host_software (host_id, software_id, last_opened_at) VALUES %s ` , values )
if _ , err := tx . ExecContext ( ctx , stmt , 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
}
2025-11-07 23:33:31 +00:00
func getExistingSoftwareSummariesByChecksums ( ctx context . Context , tx sqlx . QueryerContext , checksums [ ] string ) ( [ ] softwareSummary , error ) {
2025-09-24 22:38:13 +00:00
if len ( checksums ) == 0 {
2025-11-07 23:33:31 +00:00
return [ ] softwareSummary { } , nil
2025-09-24 22:38:13 +00:00
}
2025-11-07 23:33:31 +00:00
stmt , args , err := sqlx . In ( "SELECT name, id, checksum, title_id, bundle_identifier, source, upgrade_code FROM software WHERE checksum IN (?)" , checksums )
2024-05-15 15:34:21 +00:00
if err != nil {
2025-11-07 23:33:31 +00:00
return nil , ctxerr . Wrap ( ctx , err , "build select software summaries query" )
2024-05-15 15:34:21 +00:00
}
2025-11-07 23:33:31 +00:00
var existingSoftwareSummaries [ ] softwareSummary
if err = sqlx . SelectContext ( ctx , tx , & existingSoftwareSummaries , stmt , args ... ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "get existing software summaries" )
2024-05-15 15:34:21 +00:00
}
2025-11-07 23:33:31 +00:00
return existingSoftwareSummaries , nil
2024-05-15 15:34:21 +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 ,
2025-04-30 22:00:28 +00:00
logger log . Logger ,
2022-04-26 18:16:59 +00:00
) error {
var keysToUpdate [ ] string
for key , newSw := range incomingMap {
curSw , ok := currentMap [ key ]
2025-04-30 22:00:28 +00:00
// software must exist in current map for us to update it.
if ! ok {
continue
}
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
// if the new software has no last opened timestamp, log if the current one did
// (but only for non-apps sources, as apps sources are managed by osquery)
2025-04-30 22:00:28 +00:00
if newSw . LastOpenedAt == nil {
Adding name to software checksum for mac software (#34097)
**Related issue:** Resolves #28788
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [x] QA'd all new/changed functionality manually
## Database migrations
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* macOS app checksums now include the app name, improving grouping,
deduplication, and preventing mis-linking or duplicate entries when
multiple names share a bundle ID.
* More stable title handling when bundle IDs are missing, reducing
unintended renames and mismatches.
* **Tests**
* Re-enabled related host-software tests and added a
longest-common-prefix test to validate name reconciliation.
* **Chores**
* Database migration added to recalculate checksums for affected macOS
app records.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 22:36:34 +00:00
if curSw . LastOpenedAt != nil && newSw . Source != "apps" {
level . Info ( logger ) . Log (
"msg" , "software last_opened_at changed to nil" ,
"host_id" , hostID ,
"software_id" , curSw . ID ,
"software_name" , newSw . Name ,
"source" , newSw . Source ,
2025-04-30 22:00:28 +00:00
)
}
continue
2022-04-26 18:16:59 +00:00
}
2025-04-30 22:00:28 +00:00
// update if the new software has been opened more recently.
2024-07-16 20:18:44 +00:00
if curSw . LastOpenedAt == nil || newSw . LastOpenedAt . Sub ( * curSw . LastOpenedAt ) >= minLastOpenedAtDiff {
2022-04-26 18:16:59 +00:00
keysToUpdate = append ( keysToUpdate , key )
}
}
sort . Strings ( keysToUpdate )
2024-05-23 19:45:50 +00:00
for i := 0 ; i < len ( keysToUpdate ) ; i += softwareInsertBatchSize {
start := i
end := i + softwareInsertBatchSize
if end > len ( keysToUpdate ) {
end = len ( keysToUpdate )
}
totalToProcess := end - start
const numberOfArgsPerSoftware = 3 // number of ? in each UPDATE
// Using UNION ALL (instead of UNION) because it is faster since it does not check for duplicates.
values := strings . TrimSuffix (
strings . Repeat ( " SELECT ? as host_id, ? as software_id, ? as last_opened_at UNION ALL" , totalToProcess ) , "UNION ALL" ,
)
stmt := fmt . Sprintf (
` UPDATE host_software hs JOIN (%s) a ON hs.host_id = a.host_id AND hs.software_id = a.software_id SET hs.last_opened_at = a.last_opened_at ` ,
values ,
)
args := make ( [ ] interface { } , 0 , totalToProcess * numberOfArgsPerSoftware )
for j := start ; j < end ; j ++ {
key := keysToUpdate [ j ]
curSw , newSw := currentMap [ key ] , incomingMap [ key ]
args = append ( args , hostID , curSw . ID , newSw . LastOpenedAt )
}
if _ , err := tx . ExecContext ( ctx , stmt , args ... ) ; err != nil {
2022-04-26 18:16:59 +00:00
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.
2025-03-12 15:26:12 +00:00
// Used on software/versions not software/titles
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 ) ,
2024-07-09 17:09:16 +00:00
CreatedAt : * result . CreatedAt ,
2022-05-20 16:58:40 +00:00
}
2024-12-13 22:39:21 +00:00
if opts . IncludeCVEScores && ! opts . WithoutVulnerabilityDetails {
2022-05-20 16:58:40 +00:00
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 )
}
2025-11-04 15:04:42 +00:00
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" `
2024-07-09 17:09:16 +00:00
// CreatedAt is the time the software vulnerability was created
CreatedAt * time . Time ` db:"created_at" `
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" ,
2025-10-07 21:05:22 +00:00
"s.extension_for" ,
2022-05-20 16:58:40 +00:00
"s.release" ,
"s.vendor" ,
"s.arch" ,
2025-10-08 14:24:38 +00:00
"s.application_id" ,
2025-11-04 15:04:42 +00:00
"s.title_id" ,
2025-11-07 23:33:31 +00:00
"s.upgrade_code" ,
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" ,
2024-07-30 17:19:05 +00:00
"shc.global_stats" ,
"shc.team_id" ,
2022-08-10 21:43:22 +00:00
)
2024-10-18 17:38:26 +00:00
if opts . TeamID == nil { //nolint:gocritic // ignore ifElseChain
2024-07-30 17:19:05 +00:00
ds = ds . Where (
goqu . And (
goqu . I ( "shc.team_id" ) . Eq ( 0 ) ,
goqu . I ( "shc.global_stats" ) . Eq ( 1 ) ,
) ,
)
} else if * opts . TeamID == 0 {
ds = ds . Where (
goqu . And (
goqu . I ( "shc.team_id" ) . Eq ( 0 ) ,
goqu . I ( "shc.global_stats" ) . Eq ( 0 ) ,
) ,
)
2022-08-10 21:43:22 +00:00
} else {
2024-07-30 17:19:05 +00:00
ds = ds . Where (
goqu . And (
goqu . I ( "shc.team_id" ) . Eq ( * opts . TeamID ) ,
goqu . I ( "shc.global_stats" ) . Eq ( 0 ) ,
) ,
)
2022-08-10 21:43:22 +00:00
}
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
)
2025-10-25 21:02:02 +00:00
} else if ! opts . WithoutVulnerabilityDetails || opts . IncludeCVEScores || opts . ListOptions . MatchQuery != "" {
// LEFT JOIN software_cve if:
// 1. We need CVE details in the list (!WithoutVulnerabilityDetails), OR
// 2. We need CVE scores for ordering/filtering (IncludeCVEScores), OR
// 3. We have a search query (MatchQuery) that might be searching for CVEs
//
// When WithoutVulnerabilityDetails=true AND IncludeCVEScores=false AND MatchQuery is empty,
// we can skip this join entirely in the subquery. The outer query will fetch CVEs only for
// the paginated results, which is much more efficient.
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 {
2024-08-15 18:36:47 +00:00
baseJoinConditions := goqu . Ex {
"c.cve" : goqu . I ( "scv.cve" ) ,
}
if opts . KnownExploit || opts . MinimumCVSS > 0 || opts . MaximumCVSS > 0 {
if opts . KnownExploit {
baseJoinConditions [ "c.cisa_known_exploit" ] = true
}
if opts . MinimumCVSS > 0 {
baseJoinConditions [ "c.cvss_score" ] = goqu . Op { "gte" : opts . MinimumCVSS }
}
if opts . MaximumCVSS > 0 {
baseJoinConditions [ "c.cvss_score" ] = goqu . Op { "lte" : opts . MaximumCVSS }
}
ds = ds . InnerJoin (
2022-06-01 16:06:57 +00:00
goqu . I ( "cve_meta" ) . As ( "c" ) ,
2024-08-15 18:36:47 +00:00
goqu . On ( baseJoinConditions ) ,
2022-05-20 16:58:40 +00:00
)
2024-08-15 18:36:47 +00:00
} else {
ds = ds .
LeftJoin (
goqu . I ( "cve_meta" ) . As ( "c" ) ,
goqu . On ( baseJoinConditions ) ,
)
}
ds = ds . SelectAppend (
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-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" ,
2025-10-07 21:05:22 +00:00
"s.extension_for" ,
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" ,
2025-10-07 21:05:22 +00:00
"s.extension_for" ,
2022-05-20 16:58:40 +00:00
"s.release" ,
"s.vendor" ,
"s.arch" ,
2025-10-08 14:24:38 +00:00
"s.application_id" ,
2025-11-04 15:04:42 +00:00
"s.title_id" ,
2025-11-07 23:33:31 +00:00
"s.upgrade_code" ,
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" ,
2024-07-09 17:09:16 +00:00
"scv.created_at" ,
2022-05-20 16:58:40 +00:00
) .
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
}
Add `team_identifier` to macOS software (#23766)
Changes to add `team_identifier` signing information to macOS
applications on the `/api/latest/fleet/hosts/:id/software` API endpoint.
Docs: https://github.com/fleetdm/fleet/pull/23743
- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [X] Added/updated tests
- [X] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [X] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [X] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [X] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ X Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [X] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
---------
Co-authored-by: Tim Lee <timlee@fleetdm.com>
Co-authored-by: Ian Littman <iansltx@gmail.com>
2024-11-15 17:17:04 +00:00
installedPathsList := make ( map [ uint ] [ ] string )
pathSignatureInformation := make ( map [ uint ] [ ] fleet . PathSignatureInformation )
2023-05-17 20:53:15 +00:00
for _ , ip := range installedPaths {
Add `team_identifier` to macOS software (#23766)
Changes to add `team_identifier` signing information to macOS
applications on the `/api/latest/fleet/hosts/:id/software` API endpoint.
Docs: https://github.com/fleetdm/fleet/pull/23743
- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [X] Added/updated tests
- [X] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [X] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [X] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [X] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ X Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [X] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
---------
Co-authored-by: Tim Lee <timlee@fleetdm.com>
Co-authored-by: Ian Littman <iansltx@gmail.com>
2024-11-15 17:17:04 +00:00
installedPathsList [ ip . SoftwareID ] = append ( installedPathsList [ ip . SoftwareID ] , ip . InstalledPath )
pathSignatureInformation [ ip . SoftwareID ] = append ( pathSignatureInformation [ ip . SoftwareID ] , fleet . PathSignatureInformation {
InstalledPath : ip . InstalledPath ,
TeamIdentifier : ip . TeamIdentifier ,
2025-10-13 18:43:36 +00:00
HashSha256 : ip . ExecutableSHA256 ,
Add `team_identifier` to macOS software (#23766)
Changes to add `team_identifier` signing information to macOS
applications on the `/api/latest/fleet/hosts/:id/software` API endpoint.
Docs: https://github.com/fleetdm/fleet/pull/23743
- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [X] Added/updated tests
- [X] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [X] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [X] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [X] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ X Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [X] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
---------
Co-authored-by: Tim Lee <timlee@fleetdm.com>
Co-authored-by: Ian Littman <iansltx@gmail.com>
2024-11-15 17:17:04 +00:00
} )
2023-05-17 20:53:15 +00:00
}
host . Software = make ( [ ] fleet . HostSoftwareEntry , 0 , len ( software ) )
for _ , s := range software {
host . Software = append ( host . Software , fleet . HostSoftwareEntry {
Add `team_identifier` to macOS software (#23766)
Changes to add `team_identifier` signing information to macOS
applications on the `/api/latest/fleet/hosts/:id/software` API endpoint.
Docs: https://github.com/fleetdm/fleet/pull/23743
- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [X] Added/updated tests
- [X] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [X] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [X] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [X] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ X Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [X] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
---------
Co-authored-by: Tim Lee <timlee@fleetdm.com>
Co-authored-by: Ian Littman <iansltx@gmail.com>
2024-11-15 17:17:04 +00:00
Software : s ,
InstalledPaths : installedPathsList [ s . ID ] ,
PathSignatureInformation : pathSignatureInformation [ s . ID ] ,
2023-05-17 20:53:15 +00:00
} )
}
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
2025-10-07 21:05:22 +00:00
s . id , s . name , s . version , s . source , s . bundle_identifier , s . release , s . arch , s . vendor , s . extension_for , s . extension_id , s . title_id ,
2023-04-03 17:45:18 +00:00
COALESCE ( sc . cpe , ' ' ) AS generated_cpe
2025-11-07 23:33:31 +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
2023-02-24 18:18:25 +00:00
2023-04-03 17:45:18 +00:00
if len ( query . ExcludedSources ) != 0 {
2024-06-17 21:44:01 +00:00
conditionals = append ( conditionals , "s.source NOT IN (?)" )
args = append ( args , query . ExcludedSources )
2023-02-24 18:18:25 +00:00
}
2023-04-03 17:45:18 +00:00
if len ( query . IncludedSources ) != 0 {
2024-06-17 21:44:01 +00:00
conditionals = append ( conditionals , "s.source IN (?)" )
args = append ( args , query . IncludedSources )
}
if query . NameMatch != "" {
conditionals = append ( conditionals , "s.name REGEXP ?" )
args = append ( args , query . NameMatch )
}
if query . NameExclude != "" {
conditionals = append ( conditionals , "s.name NOT REGEXP ?" )
args = append ( args , query . NameExclude )
2023-04-03 17:45:18 +00:00
}
2022-08-04 13:24:44 +00:00
2023-04-03 17:45:18 +00:00
if len ( conditionals ) != 0 {
2024-06-17 21:44:01 +00:00
stmt += " WHERE " + strings . Join ( conditionals , " AND " )
}
stmt , args , err = sqlx . In ( stmt , args ... )
if err != nil {
return nil , fmt . Errorf ( "error building 'In' query part on software iterator: %w" , err )
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 {
2024-06-17 21:44:01 +00:00
return nil , fmt . Errorf ( "executing all software iterator %w" , err )
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-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 ) {
2024-08-15 18:36:47 +00:00
if ! opt . VulnerableOnly && ( opt . MinimumCVSS > 0 || opt . MaximumCVSS > 0 || opt . KnownExploit ) {
return nil , nil , fleet . NewInvalidArgumentError (
"query" , "min_cvss_score, max_cvss_score, and exploit can only be provided with vulnerable=true" ,
)
}
2023-12-12 18:24:20 +00:00
software , err := listSoftwareDB ( ctx , ds . reader ( ctx ) , opt )
if err != nil {
return nil , nil , err
}
2025-11-04 15:04:42 +00:00
var titleIDs [ ] uint
for _ , s := range software {
if s . TitleID != nil {
titleIDs = append ( titleIDs , * s . TitleID )
}
}
var tmID uint
if opt . TeamID != nil {
tmID = * opt . TeamID
}
displayNames , err := ds . getDisplayNamesByTeamAndTitleIds ( ctx , tmID , titleIDs )
if err != nil && ! fleet . IsNotFound ( err ) {
return nil , nil , ctxerr . Wrap ( ctx , err , "get software display names by team and title IDs" )
}
for i , s := range software {
if s . TitleID != nil {
if displayName , ok := displayNames [ * s . TitleID ] ; ok {
software [ i ] . DisplayName = displayName
}
}
}
2023-12-12 18:24:20 +00:00
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 }
2024-10-18 17:38:26 +00:00
if len ( software ) > int ( perPage ) { //nolint:gosec // dismiss G115
2023-12-12 18:24:20 +00:00
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
}
2025-07-29 15:14:14 +00:00
func ( ds * Datastore ) DeleteOutOfDateVulnerabilities ( ctx context . Context , source fleet . VulnerabilitySource , olderThan time . Time ) error {
if _ , err := ds . writer ( ctx ) . ExecContext (
ctx ,
` DELETE FROM software_cve WHERE source = ? AND updated_at < ? ` ,
source , olderThan ,
) ; 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" ,
2025-10-07 21:05:22 +00:00
"s.extension_for" ,
2022-05-20 16:58:40 +00:00
"s.bundle_identifier" ,
2025-11-07 23:33:31 +00:00
"s.upgrade_code" ,
2022-05-20 16:58:40 +00:00
"s.release" ,
"s.vendor" ,
"s.arch" ,
2023-12-12 22:51:58 +00:00
"s.extension_id" ,
2025-11-04 15:04:42 +00:00
"s.title_id" ,
2022-05-20 16:58:40 +00:00
"scv.cve" ,
2024-07-09 17:09:16 +00:00
"scv.created_at" ,
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-07-30 17:19:05 +00:00
// join only on software_id as we'll need counts for all teams
// to filter down to the team's the user has access to
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 ) )
2024-05-23 19:45:50 +00:00
// If teamID is not specified, we still return the software even if it is not associated with any hosts.
// Software is cleaned up by a cron job, so it is possible to have software in software_hosts_counts that has been deleted from a host.
if teamID != nil {
// If teamID filter is used, host counts need to be up-to-date.
// This should generally be the case, since unused software is cleared when host counts are updated.
// However, it is possible that the software was deleted from all hosts after the last host count update.
2024-02-18 13:14:20 +00:00
q = q . Where (
goqu . L (
2024-07-30 17:19:05 +00:00
"EXISTS (SELECT 1 FROM software_host_counts WHERE software_id = ? AND team_id = ? AND hosts_count > 0 AND global_stats = 0)" , id , * teamID ,
2024-02-18 13:14:20 +00:00
) ,
)
}
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
}
2025-11-04 15:04:42 +00:00
var tmID uint
if teamID != nil {
tmID = * teamID
}
if software . TitleID != nil {
displayName , err := ds . getSoftwareTitleDisplayName ( ctx , tmID , * software . TitleID )
if err != nil && ! fleet . IsNotFound ( err ) {
return nil , ctxerr . Wrap ( ctx , err , "getting display name for software" )
}
software . DisplayName = displayName
}
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 ) ,
2024-07-09 17:09:16 +00:00
CreatedAt : * result . CreatedAt ,
2022-05-20 16:58:40 +00:00
}
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 = `
2024-07-30 17:19:05 +00:00
SELECT count ( * ) , 0 as team_id , software_id , 1 as global_stats
2022-02-28 18:55:14 +00:00
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 = `
2024-07-30 17:19:05 +00:00
SELECT count ( * ) , h . team_id , hs . software_id , 0 as global_stats
2022-02-28 18:55:14 +00:00
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 `
2024-07-30 17:19:05 +00:00
noTeamCountsStmt = `
SELECT count ( * ) , 0 as team_id , software_id , 0 as global_stats
FROM host_software hs
INNER JOIN hosts h
ON hs . host_id = h . id
WHERE h . team_id IS NULL AND hs . software_id > ? AND hs . software_id <= ?
GROUP BY hs . software_id `
2022-02-28 18:55:14 +00:00
insertStmt = `
INSERT INTO software_host_counts
2024-07-30 17:19:05 +00:00
( software_id , hosts_count , team_id , global_stats , updated_at )
2022-02-28 18:55:14 +00:00
VALUES
% s
ON DUPLICATE KEY UPDATE
hosts_count = VALUES ( hosts_count ) ,
updated_at = VALUES ( updated_at ) `
2024-07-30 17:19:05 +00:00
valuesPart = ` (?, ?, ?, ?, ?), `
2022-02-28 18:55:14 +00:00
2024-05-23 19:45:50 +00:00
// We must ensure that software is not in host_software table before deleting it.
// This prevents a race condition where a host just added the software, but it is not part of software_host_counts yet.
// When a host adds software, software table and host_software table are updated in the same transaction.
2022-02-28 18:55:14 +00:00
cleanupSoftwareStmt = `
DELETE s
FROM software s
LEFT JOIN software_host_counts shc
ON s . id = shc . software_id
WHERE
2024-05-23 19:45:50 +00:00
( shc . software_id IS NULL OR
( shc . team_id = 0 AND shc . hosts_count = 0 ) ) AND
NOT EXISTS ( SELECT 1 FROM host_software hsw WHERE hsw . software_id = s . id )
`
2022-02-28 18:55:14 +00:00
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" )
}
2025-07-02 18:47:51 +00:00
if minMax . Min == 0 {
minMax . Min = 1
level . Warn ( ds . logger ) . Log ( "msg" , "software_id 0 found in host_software table; performing counts without those entries" )
}
2024-05-08 14:27:17 +00:00
for minSoftwareID , maxSoftwareID := minMax . Min - 1 , minMax . Min - 1 + countHostSoftwareBatchSize ; minSoftwareID < minMax . Max ; minSoftwareID , maxSoftwareID = maxSoftwareID , maxSoftwareID + countHostSoftwareBatchSize {
2022-01-26 14:47:56 +00:00
2024-05-08 14:27:17 +00:00
// next get a cursor for the global and team counts for each software
2024-07-30 17:19:05 +00:00
stmtLabel := [ ] string { "global" , "team" , "noteam" }
for i , countStmt := range [ ] string { globalCountsStmt , teamCountsStmt , noTeamCountsStmt } {
2024-05-08 14:27:17 +00:00
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 (
2024-07-30 17:19:05 +00:00
count int
teamID uint
sid uint
global_stats bool
2024-05-08 14:27:17 +00:00
)
2024-07-30 17:19:05 +00:00
if err := rows . Scan ( & count , & teamID , & sid , & global_stats ) ; err != nil {
2024-05-08 14:27:17 +00:00
return ctxerr . Wrapf ( ctx , err , "scan %s row into variables" , stmtLabel [ i ] )
}
2022-01-26 14:47:56 +00:00
2024-07-30 17:19:05 +00:00
args = append ( args , sid , count , teamID , global_stats , updatedAt )
2024-05-08 14:27:17 +00:00
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 ] )
}
2022-01-26 14:47:56 +00:00
2024-05-08 14:27:17 +00:00
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
2025-10-14 22:36:45 +00:00
func ( ds * Datastore ) CleanupSoftwareTitles ( ctx context . Context ) error {
var n int64
defer func ( start time . Time ) {
level . Debug ( ds . logger ) . Log (
"msg" , "cleanup orphaned software titles" ,
"rows_affected" , n ,
"took" , time . Since ( start ) ,
)
} ( time . Now ( ) )
separate queries for software matching to use indexes (#20354)
during load testing we found this query to be a bottleneck, locally
splitting in to two different statements makes a difference since we can
effectively use the indexes.
```
mysql> explain UPDATE software s
-> JOIN software_titles st
-> ON COALESCE(s.bundle_identifier, '') = '' AND s.name = st.name AND s.source = st.source AND s.browser = st.browser
-> SET s.title_id = st.id
-> WHERE (s.title_id IS NULL OR s.title_id != st.id)
-> AND COALESCE(s.bundle_identifier, '') = '';
+----+-------------+-------+------------+-------+-------------------------------------------------------------------------------------+----------------------------+---------+------------------------------------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+-------------------------------------------------------------------------------------+----------------------------+---------+------------------------------------------------+------+----------+-------------+
| 1 | SIMPLE | st | NULL | index | idx_sw_titles | idx_sw_titles | 2302 | NULL | 765 | 100.00 | Using index |
| 1 | UPDATE | s | NULL | ref | software_listing_idx,software_source_vendor_idx,title_id,idx_sw_name_source_browser | idx_sw_name_source_browser | 2302 | fleet.st.name,fleet.st.source,fleet.st.browser | 1 | 91.00 | Using where |
+----+-------------+-------+------------+-------+-------------------------------------------------------------------------------------+----------------------------+---------+------------------------------------------------+------+----------+-------------+
2 rows in set (0.00 sec)
mysql> explain UPDATE software s
-> JOIN software_titles st
-> ON s.bundle_identifier = st.bundle_identifier
-> SET s.title_id = st.id
-> WHERE s.title_id IS NULL
-> OR s.title_id != st.id;
+----+-------------+-------+------------+------+-----------------------------------------------------+---------------------------------------+---------+---------------------------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+-----------------------------------------------------+---------------------------------------+---------+---------------------------+------+----------+--------------------------+
| 1 | UPDATE | s | NULL | ALL | title_id,idx_software_bundle_id | NULL | NULL | NULL | 788 | 100.00 | Using where |
| 1 | SIMPLE | st | NULL | ref | idx_software_titles_bundle_identifier,idx_composite | idx_software_titles_bundle_identifier | 1023 | fleet.s.bundle_identifier | 1 | 100.00 | Using where; Using index |
+----+-------------+-------+------------+------+-----------------------------------------------------+---------------------------------------+---------+---------------------------+------+----------+--------------------------+
2 rows in set (0.00 sec)
```
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [x] Manual QA for all new/changed functionality
2024-07-10 21:16:38 +00:00
2025-10-14 22:36:45 +00:00
const deleteOrphanedSoftwareTitlesStmt = `
DELETE st FROM software_titles st
LEFT JOIN software s ON st . id = s . title_id
LEFT JOIN software_installers si ON st . id = si . title_id
2025-10-28 12:33:58 +00:00
LEFT JOIN in_house_apps iha ON st . id = iha . title_id
2025-10-14 22:36:45 +00:00
LEFT JOIN vpp_apps vap ON st . id = vap . title_id
2025-10-28 12:33:58 +00:00
WHERE s . title_id IS NULL AND si . title_id IS NULL AND iha . title_id IS NULL AND vap . title_id IS NULL `
2023-12-04 16:09:23 +00:00
2025-10-14 22:36:45 +00:00
res , err := ds . writer ( ctx ) . ExecContext ( ctx , deleteOrphanedSoftwareTitlesStmt )
if err != nil {
return ctxerr . Wrap ( ctx , err , "executing delete of software titles" )
}
ra , _ := res . RowsAffected ( )
n += ra
2025-04-11 23:19:07 +00:00
2025-10-14 22:36:45 +00:00
return nil
2023-12-04 16:09:23 +00:00
}
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
2025-04-11 23:19:07 +00:00
// Deprecated: ** DEPRECATED **
2023-05-17 20:53:15 +00:00
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
}
2024-08-30 21:00:35 +00:00
return insertOnDuplicateDidInsertOrUpdate ( 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 ,
2024-07-09 17:50:22 +00:00
filters fleet . VulnSoftwareFilter ,
2022-06-08 01:09:47 +00:00
) ( [ ] fleet . Software , error ) {
var result [ ] fleet . Software
2024-07-09 17:50:22 +00:00
var sqlstmt string
var args [ ] interface { }
2022-06-08 01:09:47 +00:00
2024-07-09 17:50:22 +00:00
baseSQL := `
2024-07-16 20:18:44 +00:00
SELECT
2024-07-09 17:50:22 +00:00
s . id ,
s . name ,
s . version ,
s . release ,
s . arch ,
COALESCE ( cpe . cpe , ' ' ) AS generated_cpe
2024-07-16 20:18:44 +00:00
FROM
2024-07-09 17:50:22 +00:00
software s
2024-07-16 20:18:44 +00:00
LEFT JOIN
2024-07-09 17:50:22 +00:00
software_cpe cpe ON s . id = cpe . software_id
`
2022-06-08 01:09:47 +00:00
2024-07-09 17:50:22 +00:00
if filters . HostID != nil {
baseSQL += "JOIN host_software hs ON s.id = hs.software_id "
}
conditions := [ ] string { }
if filters . HostID != nil {
conditions = append ( conditions , "hs.host_id = ?" )
args = append ( args , * filters . HostID )
}
if filters . Name != "" {
conditions = append ( conditions , "s.name LIKE ?" )
args = append ( args , "%" + filters . Name + "%" )
}
if filters . Source != "" {
conditions = append ( conditions , "s.source = ?" )
args = append ( args , filters . Source )
}
if len ( conditions ) > 0 {
sqlstmt = baseSQL + "WHERE " + strings . Join ( conditions , " AND " )
} else {
sqlstmt = baseSQL
2022-06-08 01:09:47 +00:00
}
2024-07-09 17:50:22 +00:00
if err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & result , sqlstmt , 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
}
2024-05-01 18:37:52 +00:00
2025-11-07 23:33:31 +00:00
// TODO(jacob) SoftwareUpgradeCode ? SoftwareUpgradeCodeList ?
2025-04-10 22:29:15 +00:00
type hostSoftware struct {
fleet . HostSoftwareWithInstaller
LastInstallInstalledAt * time . Time ` db:"last_install_installed_at" `
LastInstallInstallUUID * string ` db:"last_install_install_uuid" `
LastUninstallUninstalledAt * time . Time ` db:"last_uninstall_uninstalled_at" `
LastUninstallScriptExecutionID * string ` db:"last_uninstall_script_execution_id" `
2025-11-07 22:30:51 +00:00
ExitCode * int ` db:"exit_code" `
LastOpenedAt * time . Time ` db:"last_opened_at" `
BundleIdentifier * string ` db:"bundle_identifier" `
Version * string ` db:"version" `
SoftwareID * uint ` db:"software_id" `
SoftwareSource * string ` db:"software_source" `
SoftwareExtensionFor * string ` db:"software_extension_for" `
InstallerID * uint ` db:"installer_id" `
PackageSelfService * bool ` db:"package_self_service" `
PackageName * string ` db:"package_name" `
PackagePlatform * string ` db:"package_platform" `
PackageVersion * string ` db:"package_version" `
VPPAppSelfService * bool ` db:"vpp_app_self_service" `
VPPAppAdamID * string ` db:"vpp_app_adam_id" `
VPPAppVersion * string ` db:"vpp_app_version" `
VPPAppPlatform * string ` db:"vpp_app_platform" `
VPPAppIconURL * string ` db:"vpp_app_icon_url" `
InHouseAppID * uint ` db:"in_house_app_id" `
InHouseAppName * string ` db:"in_house_app_name" `
InHouseAppPlatform * string ` db:"in_house_app_platform" `
InHouseAppVersion * string ` db:"in_house_app_version" `
InHouseAppSelfService * bool ` db:"in_house_app_self_service" `
VulnerabilitiesList * string ` db:"vulnerabilities_list" `
SoftwareIDList * string ` db:"software_id_list" `
SoftwareSourceList * string ` db:"software_source_list" `
SoftwareExtensionForList * string ` db:"software_extension_for_list" `
VersionList * string ` db:"version_list" `
BundleIdentifierList * string ` db:"bundle_identifier_list" `
VPPAppSelfServiceList * string ` db:"vpp_app_self_service_list" `
VPPAppAdamIDList * string ` db:"vpp_app_adam_id_list" `
VPPAppVersionList * string ` db:"vpp_app_version_list" `
VPPAppPlatformList * string ` db:"vpp_app_platform_list" `
VPPAppIconUrlList * string ` db:"vpp_app_icon_url_list" `
InHouseAppIDList * string ` db:"in_house_app_id_list" `
InHouseAppNameList * string ` db:"in_house_app_name_list" `
InHouseAppPlatformList * string ` db:"in_house_app_platform_list" `
InHouseAppVersionList * string ` db:"in_house_app_version_list" `
InHouseAppSelfServiceList * string ` db:"in_house_app_self_service_list" `
2025-11-07 23:33:31 +00:00
SoftwareUpgradeCodeList * string ` db:"software_upgrade_code_list" `
2025-04-10 22:29:15 +00:00
}
func hostInstalledSoftware ( ds * Datastore , ctx context . Context , hostID uint ) ( [ ] * hostSoftware , error ) {
2025-11-07 23:33:31 +00:00
// TODO(jacob)?: software_titles.upgrade_code AS upgrade_code,
2025-04-10 22:29:15 +00:00
installedSoftwareStmt := `
SELECT
software_titles . id AS id ,
host_software . software_id AS software_id ,
host_software . last_opened_at ,
2025-04-22 03:53:06 +00:00
software . source AS software_source ,
2025-10-07 21:05:22 +00:00
software . extension_for AS software_extension_for ,
2025-04-22 03:53:06 +00:00
software . version AS version ,
2025-04-22 22:00:19 +00:00
software . bundle_identifier AS bundle_identifier
2025-08-14 14:13:37 +00:00
FROM
2025-04-10 22:29:15 +00:00
host_software
INNER JOIN
software ON host_software . software_id = software . id
INNER JOIN
software_titles ON software . title_id = software_titles . id
WHERE
host_software . host_id = ?
`
var hostInstalledSoftware [ ] * hostSoftware
err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & hostInstalledSoftware , installedSoftwareStmt , hostID )
if err != nil {
return nil , err
2025-03-12 15:26:12 +00:00
}
2025-04-10 22:29:15 +00:00
return hostInstalledSoftware , nil
}
func hostSoftwareInstalls ( ds * Datastore , ctx context . Context , hostID uint ) ( [ ] * hostSoftware , error ) {
softwareInstallsStmt := `
WITH upcoming_software_install AS (
SELECT
ua . execution_id AS last_install_install_uuid ,
ua . created_at AS last_install_installed_at ,
siua . software_installer_id AS installer_id ,
' pending_install ' AS status
FROM
upcoming_activities ua
INNER JOIN
software_install_upcoming_activities siua ON ua . id = siua . upcoming_activity_id
LEFT JOIN (
upcoming_activities ua2
INNER JOIN software_install_upcoming_activities siua2 ON ua2 . id = siua2 . upcoming_activity_id
) ON ua . host_id = ua2 . host_id AND
siua . software_installer_id = siua2 . software_installer_id AND
ua . activity_type = ua2 . activity_type AND
( ua2 . priority < ua . priority OR ua2 . created_at > ua . created_at )
WHERE
ua . host_id = ? AND
ua . activity_type = ' software_install ' AND
ua2 . id IS NULL
) ,
last_software_install AS (
SELECT
hsi . execution_id AS last_install_install_uuid ,
hsi . updated_at AS last_install_installed_at ,
hsi . software_installer_id AS installer_id ,
hsi . status AS status
FROM
host_software_installs hsi
LEFT JOIN
host_software_installs hsi2 ON hsi . host_id = hsi2 . host_id AND
hsi . software_installer_id = hsi2 . software_installer_id AND
hsi . uninstall = hsi2 . uninstall AND
hsi2 . removed = 0 AND
hsi2 . canceled = 0 AND
hsi2 . host_deleted_at IS NULL AND
( hsi . created_at < hsi2 . created_at OR ( hsi . created_at = hsi2 . created_at AND hsi . id < hsi2 . id ) )
WHERE
hsi . host_id = ? AND
hsi . removed = 0 AND
hsi . canceled = 0 AND
hsi . uninstall = 0 AND
hsi . host_deleted_at IS NULL AND
hsi2 . id IS NULL AND
NOT EXISTS (
SELECT 1
FROM
upcoming_activities ua
INNER JOIN
software_install_upcoming_activities siua ON ua . id = siua . upcoming_activity_id
WHERE
ua . host_id = hsi . host_id AND
siua . software_installer_id = hsi . software_installer_id AND
ua . activity_type = ' software_install '
)
)
SELECT
software_installers . id AS installer_id ,
2025-05-06 17:32:35 +00:00
software_installers . self_service AS package_self_service ,
2025-04-10 22:29:15 +00:00
software_titles . id AS id ,
lsia . *
FROM
( SELECT * FROM upcoming_software_install UNION SELECT * FROM last_software_install ) AS lsia
INNER JOIN
software_installers ON lsia . installer_id = software_installers . id
INNER JOIN
software_titles ON software_installers . title_id = software_titles . id
`
var softwareInstalls [ ] * hostSoftware
err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & softwareInstalls , softwareInstallsStmt , hostID , hostID )
if err != nil {
return nil , err
2024-05-27 14:53:41 +00:00
}
2024-07-09 14:02:49 +00:00
2025-04-10 22:29:15 +00:00
return softwareInstalls , nil
}
func hostSoftwareUninstalls ( ds * Datastore , ctx context . Context , hostID uint ) ( [ ] * hostSoftware , error ) {
softwareUninstallsStmt := `
WITH upcoming_software_uninstall AS (
SELECT
ua . execution_id AS last_uninstall_script_execution_id ,
ua . created_at AS last_uninstall_uninstalled_at ,
siua . software_installer_id AS installer_id ,
' pending_uninstall ' AS status
FROM
upcoming_activities ua
INNER JOIN
software_install_upcoming_activities siua ON ua . id = siua . upcoming_activity_id
LEFT JOIN (
upcoming_activities ua2
INNER JOIN software_install_upcoming_activities siua2 ON ua2 . id = siua2 . upcoming_activity_id
) ON ua . host_id = ua2 . host_id AND
siua . software_installer_id = siua2 . software_installer_id AND
ua . activity_type = ua2 . activity_type AND
( ua2 . priority < ua . priority OR ua2 . created_at > ua . created_at )
WHERE
ua . host_id = ? AND
ua . activity_type = ' software_uninstall ' AND
ua2 . id IS NULL
) ,
last_software_uninstall AS (
SELECT
hsi . execution_id AS last_uninstall_script_execution_id ,
hsi . updated_at AS last_uninstall_uninstalled_at ,
hsi . software_installer_id AS installer_id ,
hsi . status AS status
FROM
host_software_installs hsi
LEFT JOIN
host_software_installs hsi2 ON hsi . host_id = hsi2 . host_id AND
hsi . software_installer_id = hsi2 . software_installer_id AND
hsi . uninstall = hsi2 . uninstall AND
hsi2 . removed = 0 AND
hsi2 . canceled = 0 AND
hsi2 . host_deleted_at IS NULL AND
( hsi . created_at < hsi2 . created_at OR ( hsi . created_at = hsi2 . created_at AND hsi . id < hsi2 . id ) )
WHERE
hsi . host_id = ? AND
hsi . removed = 0 AND
hsi . uninstall = 1 AND
hsi . canceled = 0 AND
hsi . host_deleted_at IS NULL AND
hsi2 . id IS NULL AND
NOT EXISTS (
SELECT 1
FROM
upcoming_activities ua
INNER JOIN
software_install_upcoming_activities siua ON ua . id = siua . upcoming_activity_id
WHERE
ua . host_id = hsi . host_id AND
siua . software_installer_id = hsi . software_installer_id AND
ua . activity_type = ' software_uninstall '
)
)
SELECT
software_installers . id AS installer_id ,
software_titles . id AS id ,
host_script_results . exit_code AS exit_code ,
lsua . *
FROM
( SELECT * FROM upcoming_software_uninstall UNION SELECT * FROM last_software_uninstall ) AS lsua
INNER JOIN
software_installers ON lsua . installer_id = software_installers . id
INNER JOIN
software_titles ON software_installers . title_id = software_titles . id
LEFT OUTER JOIN
host_script_results ON host_script_results . host_id = ? AND host_script_results . execution_id = lsua . last_uninstall_script_execution_id
`
var softwareUninstalls [ ] * hostSoftware
err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & softwareUninstalls , softwareUninstallsStmt , hostID , hostID , hostID )
if err != nil {
return nil , err
2024-10-28 19:48:54 +00:00
}
2025-04-10 22:29:15 +00:00
return softwareUninstalls , nil
}
2025-03-12 15:26:12 +00:00
2025-04-10 22:29:15 +00:00
func filterSoftwareInstallersByLabel (
ds * Datastore ,
ctx context . Context ,
host * fleet . Host ,
bySoftwareTitleID map [ uint ] * hostSoftware ,
) ( map [ uint ] * hostSoftware , error ) {
if len ( bySoftwareTitleID ) == 0 {
return bySoftwareTitleID , nil
}
filteredbySoftwareTitleID := make ( map [ uint ] * hostSoftware , len ( bySoftwareTitleID ) )
softwareInstallersIDsToCheck := make ( [ ] uint , 0 , len ( bySoftwareTitleID ) )
for _ , st := range bySoftwareTitleID {
2025-05-06 17:32:35 +00:00
if st . InstallerID != nil {
2025-04-10 22:29:15 +00:00
softwareInstallersIDsToCheck = append ( softwareInstallersIDsToCheck , * st . InstallerID )
}
}
if len ( softwareInstallersIDsToCheck ) > 0 {
labelSqlFilter := `
WITH no_labels AS (
SELECT
software_installers . id AS installer_id ,
0 AS count_installer_labels ,
0 AS count_host_labels ,
0 AS count_host_updated_after_labels
FROM
software_installers
WHERE NOT EXISTS (
SELECT 1
FROM software_installer_labels
WHERE software_installer_labels . software_installer_id = software_installers . id
)
) ,
include_any AS (
SELECT
software_installers . id AS installer_id ,
COUNT ( * ) AS count_installer_labels ,
COUNT ( label_membership . label_id ) AS count_host_labels ,
0 AS count_host_updated_after_labels
FROM
software_installers
INNER JOIN software_installer_labels
ON software_installer_labels . software_installer_id = software_installers . id AND software_installer_labels . exclude = 0
LEFT JOIN label_membership
ON label_membership . label_id = software_installer_labels . label_id
AND label_membership . host_id = : host_id
GROUP BY
software_installers . id
HAVING
COUNT ( * ) > 0 AND COUNT ( label_membership . label_id ) > 0
) ,
exclude_any AS (
SELECT
software_installers . id AS installer_id ,
COUNT ( software_installer_labels . label_id ) AS count_installer_labels ,
COUNT ( label_membership . label_id ) AS count_host_labels ,
SUM (
CASE
2025-07-11 17:18:34 +00:00
WHEN labels . created_at IS NOT NULL AND (
labels . label_membership_type = 1 OR
( labels . label_membership_type = 0 AND : host_label_updated_at >= labels . created_at )
) THEN 1
2025-04-10 22:29:15 +00:00
ELSE 0
END
) AS count_host_updated_after_labels
FROM
software_installers
INNER JOIN software_installer_labels
ON software_installer_labels . software_installer_id = software_installers . id AND software_installer_labels . exclude = 1
INNER JOIN labels
ON labels . id = software_installer_labels . label_id
LEFT JOIN label_membership
ON label_membership . label_id = software_installer_labels . label_id
AND label_membership . host_id = : host_id
GROUP BY
software_installers . id
HAVING
COUNT ( * ) > 0
AND COUNT ( * ) = SUM (
CASE
2025-07-11 17:18:34 +00:00
WHEN labels . created_at IS NOT NULL AND (
labels . label_membership_type = 1 OR
( labels . label_membership_type = 0 AND : host_label_updated_at >= labels . created_at )
) THEN 1
2025-04-10 22:29:15 +00:00
ELSE 0
END
)
AND COUNT ( label_membership . label_id ) = 0
)
SELECT
software_installers . id AS id ,
software_installers . title_id AS title_id
FROM
software_installers
LEFT JOIN no_labels
ON no_labels . installer_id = software_installers . id
LEFT JOIN include_any
ON include_any . installer_id = software_installers . id
LEFT JOIN exclude_any
ON exclude_any . installer_id = software_installers . id
WHERE
software_installers . id IN ( : software_installer_ids )
AND (
no_labels . installer_id IS NOT NULL
OR include_any . installer_id IS NOT NULL
OR exclude_any . installer_id IS NOT NULL
)
2024-07-09 14:02:49 +00:00
`
2025-04-10 22:29:15 +00:00
labelSqlFilter , args , err := sqlx . Named ( labelSqlFilter , map [ string ] any {
"host_id" : host . ID ,
"host_label_updated_at" : host . LabelUpdatedAt ,
"software_installer_ids" : softwareInstallersIDsToCheck ,
} )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "filterSoftwareInstallersByLabel building named query args" )
}
2025-03-12 15:26:12 +00:00
2025-04-10 22:29:15 +00:00
labelSqlFilter , args , err = sqlx . In ( labelSqlFilter , args ... )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "filterSoftwareInstallersByLabel building in query args" )
2025-03-12 15:26:12 +00:00
}
2025-04-10 22:29:15 +00:00
labelSqlFilter = ds . reader ( ctx ) . Rebind ( labelSqlFilter )
var validSoftwareInstallers [ ] struct {
Id uint ` db:"id" `
TitleId uint ` db:"title_id" `
2025-03-12 15:26:12 +00:00
}
2025-04-10 22:29:15 +00:00
err = sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & validSoftwareInstallers , labelSqlFilter , args ... )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "filterSoftwareInstallersByLabel executing query" )
2025-03-12 15:26:12 +00:00
}
2025-04-10 22:29:15 +00:00
// go through the returned list of validSoftwareInstaller and add all the titles that meet the label criteria to be returned
for _ , validSoftwareInstaller := range validSoftwareInstallers {
filteredbySoftwareTitleID [ validSoftwareInstaller . TitleId ] = bySoftwareTitleID [ validSoftwareInstaller . TitleId ]
2025-03-12 15:26:12 +00:00
}
2024-07-09 14:02:49 +00:00
}
2025-04-10 22:29:15 +00:00
return filteredbySoftwareTitleID , nil
}
2024-12-20 21:56:51 +00:00
2025-10-28 12:33:58 +00:00
func filterVPPAppsByLabel (
2025-04-22 22:00:19 +00:00
ds * Datastore ,
ctx context . Context ,
host * fleet . Host ,
byVppAppID map [ string ] * hostSoftware ,
2025-05-06 17:32:35 +00:00
hostVPPInstalledTitles map [ uint ] * hostSoftware ,
) ( map [ string ] * hostSoftware , map [ string ] * hostSoftware , error ) {
2025-04-22 22:00:19 +00:00
filteredbyVppAppID := make ( map [ string ] * hostSoftware , len ( byVppAppID ) )
2025-05-06 17:32:35 +00:00
otherVppAppsInInventory := make ( map [ string ] * hostSoftware , len ( hostVPPInstalledTitles ) )
// This is the list of VPP apps that are installed on the host by fleet or the user
// that we want to check are in scope or not
2025-04-22 22:00:19 +00:00
vppAppIDsToCheck := make ( [ ] string , 0 , len ( byVppAppID ) )
for _ , st := range byVppAppID {
2025-05-06 17:32:35 +00:00
vppAppIDsToCheck = append ( vppAppIDsToCheck , * st . VPPAppAdamID )
}
for _ , st := range hostVPPInstalledTitles {
if st . VPPAppAdamID != nil {
2025-04-22 22:00:19 +00:00
vppAppIDsToCheck = append ( vppAppIDsToCheck , * st . VPPAppAdamID )
}
}
if len ( vppAppIDsToCheck ) > 0 {
2025-05-06 17:32:35 +00:00
var globalOrTeamID uint
if host . TeamID != nil {
globalOrTeamID = * host . TeamID
}
2025-04-22 22:00:19 +00:00
labelSqlFilter := `
WITH no_labels AS (
SELECT
vpp_apps_teams . id AS team_id ,
0 AS count_installer_labels ,
0 AS count_host_labels ,
0 as count_host_updated_after_labels
FROM
vpp_apps_teams
WHERE NOT EXISTS (
SELECT 1
FROM vpp_app_team_labels
WHERE vpp_app_team_labels . vpp_app_team_id = vpp_apps_teams . id
)
) ,
include_any AS (
SELECT
vpp_apps_teams . id AS team_id ,
COUNT ( vpp_app_team_labels . label_id ) AS count_installer_labels ,
COUNT ( label_membership . label_id ) AS count_host_labels ,
0 as count_host_updated_after_labels
FROM
vpp_apps_teams
INNER JOIN vpp_app_team_labels
ON vpp_app_team_labels . vpp_app_team_id = vpp_apps_teams . id AND vpp_app_team_labels . exclude = 0
LEFT JOIN label_membership
ON label_membership . label_id = vpp_app_team_labels . label_id
AND label_membership . host_id = : host_id
GROUP BY
vpp_apps_teams . id
HAVING
count_installer_labels > 0 AND count_host_labels > 0
) ,
exclude_any AS (
SELECT
vpp_apps_teams . id AS team_id ,
COUNT ( vpp_app_team_labels . label_id ) AS count_installer_labels ,
COUNT ( label_membership . label_id ) AS count_host_labels ,
SUM (
CASE
WHEN labels . created_at IS NOT NULL AND labels . label_membership_type = 0 AND : host_label_updated_at >= labels . created_at THEN 1
WHEN labels . created_at IS NOT NULL AND labels . label_membership_type = 1 THEN 1
ELSE 0
END
) AS count_host_updated_after_labels
FROM
vpp_apps_teams
INNER JOIN vpp_app_team_labels
ON vpp_app_team_labels . vpp_app_team_id = vpp_apps_teams . id AND vpp_app_team_labels . exclude = 1
INNER JOIN labels
ON labels . id = vpp_app_team_labels . label_id
LEFT OUTER JOIN label_membership
ON label_membership . label_id = vpp_app_team_labels . label_id AND label_membership . host_id = : host_id
GROUP BY
vpp_apps_teams . id
HAVING
count_installer_labels > 0
AND count_installer_labels = count_host_updated_after_labels
AND count_host_labels = 0
)
SELECT
vpp_apps . adam_id AS adam_id ,
vpp_apps . title_id AS title_id
FROM
vpp_apps
INNER JOIN
vpp_apps_teams ON vpp_apps . adam_id = vpp_apps_teams . adam_id AND vpp_apps . platform = vpp_apps_teams . platform AND vpp_apps_teams . global_or_team_id = : global_or_team_id
LEFT JOIN no_labels
ON no_labels . team_id = vpp_apps_teams . id
LEFT JOIN include_any
ON include_any . team_id = vpp_apps_teams . id
LEFT JOIN exclude_any
ON exclude_any . team_id = vpp_apps_teams . id
WHERE
vpp_apps . adam_id IN ( : vpp_app_adam_ids )
AND (
no_labels . team_id IS NOT NULL
OR include_any . team_id IS NOT NULL
OR exclude_any . team_id IS NOT NULL
)
`
labelSqlFilter , args , err := sqlx . Named ( labelSqlFilter , map [ string ] any {
"host_id" : host . ID ,
"host_label_updated_at" : host . LabelUpdatedAt ,
"vpp_app_adam_ids" : vppAppIDsToCheck ,
"global_or_team_id" : globalOrTeamID ,
} )
if err != nil {
2025-05-06 17:32:35 +00:00
return nil , nil , ctxerr . Wrap ( ctx , err , "filterVppAppsByLabel building named query args" )
2025-04-22 22:00:19 +00:00
}
labelSqlFilter , args , err = sqlx . In ( labelSqlFilter , args ... )
if err != nil {
2025-05-06 17:32:35 +00:00
return nil , nil , ctxerr . Wrap ( ctx , err , "filterVppAppsByLabel building in query args" )
2025-04-22 22:00:19 +00:00
}
var validVppApps [ ] struct {
AdamId string ` db:"adam_id" `
TitleId uint ` db:"title_id" `
}
err = sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & validVppApps , labelSqlFilter , args ... )
if err != nil {
2025-05-06 17:32:35 +00:00
return nil , nil , ctxerr . Wrap ( ctx , err , "filterVppAppsByLabel executing query" )
2025-04-22 22:00:19 +00:00
}
2025-05-06 17:32:35 +00:00
// differentiate between VPP apps that were installed by Fleet (show install details +
// ability to reinstall in self-service) vs. VPP apps that Fleet knows about but either
// weren't installed by Fleet or were installed by Fleet but are no longer in scope
// (treat as in inventory and not re-installable in self-service)
2025-04-22 22:00:19 +00:00
for _ , validAppApp := range validVppApps {
2025-05-06 17:32:35 +00:00
if _ , ok := byVppAppID [ validAppApp . AdamId ] ; ok {
filteredbyVppAppID [ validAppApp . AdamId ] = byVppAppID [ validAppApp . AdamId ]
} else if svpp , ok := hostVPPInstalledTitles [ validAppApp . TitleId ] ; ok {
otherVppAppsInInventory [ validAppApp . AdamId ] = svpp
}
2025-04-22 22:00:19 +00:00
}
}
2025-05-06 17:32:35 +00:00
return filteredbyVppAppID , otherVppAppsInInventory , nil
2025-04-22 22:00:19 +00:00
}
2025-10-28 12:33:58 +00:00
func filterInHouseAppsByLabel (
ds * Datastore ,
ctx context . Context ,
host * fleet . Host ,
byInHouseID map [ uint ] * hostSoftware ,
hostInHouseInstalledTitles map [ uint ] * hostSoftware ,
) ( map [ uint ] * hostSoftware , map [ uint ] * hostSoftware , error ) {
filteredByInHouseID := make ( map [ uint ] * hostSoftware , len ( byInHouseID ) )
otherInHouseAppsInInventory := make ( map [ uint ] * hostSoftware , len ( hostInHouseInstalledTitles ) )
// This is the list of in-house apps that are installed on the host by fleet or the user
// that we want to check are in scope or not
inHouseIDsToCheck := make ( [ ] uint , 0 , len ( byInHouseID ) )
for _ , st := range byInHouseID {
inHouseIDsToCheck = append ( inHouseIDsToCheck , * st . InHouseAppID )
}
for _ , st := range hostInHouseInstalledTitles {
if st . InHouseAppID != nil {
inHouseIDsToCheck = append ( inHouseIDsToCheck , * st . InHouseAppID )
}
}
if len ( inHouseIDsToCheck ) == 0 {
return filteredByInHouseID , otherInHouseAppsInInventory , nil
}
var globalOrTeamID uint
if host . TeamID != nil {
globalOrTeamID = * host . TeamID
}
labelSQLFilter := `
WITH no_labels AS (
SELECT
iha . id AS in_house_app_id ,
0 AS count_installer_labels ,
0 AS count_host_labels ,
0 as count_host_updated_after_labels
FROM
in_house_apps iha
WHERE NOT EXISTS (
SELECT 1
FROM in_house_app_labels ihl
WHERE ihl . in_house_app_id = iha . id
)
) ,
include_any AS (
SELECT
iha . id AS in_house_app_id ,
COUNT ( ihl . label_id ) AS count_installer_labels ,
COUNT ( lm . label_id ) AS count_host_labels ,
0 as count_host_updated_after_labels
FROM
in_house_apps iha
INNER JOIN in_house_app_labels ihl ON
ihl . in_house_app_id = iha . id AND ihl . exclude = 0
LEFT JOIN label_membership lm ON
lm . label_id = ihl . label_id AND lm . host_id = : host_id
GROUP BY
iha . id
HAVING
count_installer_labels > 0 AND count_host_labels > 0
) ,
exclude_any AS (
SELECT
iha . id AS in_house_app_id ,
COUNT ( ihl . label_id ) AS count_installer_labels ,
COUNT ( lm . label_id ) AS count_host_labels ,
SUM (
CASE
WHEN lbl . created_at IS NOT NULL AND lbl . label_membership_type = 0 AND : host_label_updated_at >= lbl . created_at THEN 1
WHEN lbl . created_at IS NOT NULL AND lbl . label_membership_type = 1 THEN 1
ELSE 0
END
) AS count_host_updated_after_labels
FROM
in_house_apps iha
INNER JOIN in_house_app_labels ihl ON
ihl . in_house_app_id = iha . id AND ihl . exclude = 1
INNER JOIN labels lbl ON
lbl . id = ihl . label_id
LEFT OUTER JOIN label_membership lm ON
lm . label_id = ihl . label_id AND lm . host_id = : host_id
GROUP BY
iha . id
HAVING
count_installer_labels > 0 AND
count_installer_labels = count_host_updated_after_labels AND
count_host_labels = 0
)
SELECT
iha . id AS in_house_id ,
iha . title_id AS title_id
FROM
in_house_apps iha
LEFT JOIN no_labels
ON no_labels . in_house_app_id = iha . id
LEFT JOIN include_any
ON include_any . in_house_app_id = iha . id
LEFT JOIN exclude_any
ON exclude_any . in_house_app_id = iha . id
WHERE
iha . global_or_team_id = : global_or_team_id AND
iha . id IN ( : in_house_ids ) AND (
no_labels . in_house_app_id IS NOT NULL OR
include_any . in_house_app_id IS NOT NULL OR
exclude_any . in_house_app_id IS NOT NULL
)
`
labelSQLFilter , args , err := sqlx . Named ( labelSQLFilter , map [ string ] any {
"host_id" : host . ID ,
"host_label_updated_at" : host . LabelUpdatedAt ,
"in_house_ids" : inHouseIDsToCheck ,
"global_or_team_id" : globalOrTeamID ,
} )
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "filterInHouseAppsByLabel building named query args" )
}
labelSQLFilter , args , err = sqlx . In ( labelSQLFilter , args ... )
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "filterInHouseAppsByLabel building in query args" )
}
var validInHouseApps [ ] struct {
InHouseID uint ` db:"in_house_id" `
TitleID uint ` db:"title_id" `
}
err = sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & validInHouseApps , labelSQLFilter , args ... )
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "filterInHouseAppsByLabel executing query" )
}
// differentiate between in-house apps that were installed by Fleet (show install details +
// ability to reinstall in self-service) vs. in-house apps that Fleet knows about but either
// weren't installed by Fleet or were installed by Fleet but are no longer in scope
// (treat as in inventory and not re-installable in self-service)
for _ , validApp := range validInHouseApps {
if _ , ok := byInHouseID [ validApp . InHouseID ] ; ok {
filteredByInHouseID [ validApp . InHouseID ] = byInHouseID [ validApp . InHouseID ]
} else if installed , ok := hostInHouseInstalledTitles [ validApp . TitleID ] ; ok {
otherInHouseAppsInInventory [ validApp . InHouseID ] = installed
}
}
return filteredByInHouseID , otherInHouseAppsInInventory , nil
}
2025-04-10 22:29:15 +00:00
func hostVPPInstalls ( ds * Datastore , ctx context . Context , hostID uint , globalOrTeamID uint , selfServiceOnly bool , isMDMEnrolled bool ) ( [ ] * hostSoftware , error ) {
var selfServiceFilter string
if selfServiceOnly {
if isMDMEnrolled {
selfServiceFilter = "(vat.self_service = 1) AND "
} else {
selfServiceFilter = "FALSE AND "
}
}
vppInstallsStmt := fmt . Sprintf ( `
( -- upcoming_vpp_install
SELECT
2025-04-14 20:27:43 +00:00
vpp_apps . title_id AS id ,
2025-04-10 22:29:15 +00:00
ua . execution_id AS last_install_install_uuid ,
ua . created_at AS last_install_installed_at ,
vaua . adam_id AS vpp_app_adam_id ,
2025-05-20 15:44:27 +00:00
vat . self_service AS vpp_app_self_service ,
2025-04-10 22:29:15 +00:00
' pending_install ' AS status
FROM
upcoming_activities ua
INNER JOIN
vpp_app_upcoming_activities vaua ON ua . id = vaua . upcoming_activity_id
LEFT JOIN (
upcoming_activities ua2
INNER JOIN vpp_app_upcoming_activities vaua2 ON ua2 . id = vaua2 . upcoming_activity_id
) ON ua . host_id = ua2 . host_id AND
vaua . adam_id = vaua2 . adam_id AND
vaua . platform = vaua2 . platform AND
ua . activity_type = ua2 . activity_type AND
( ua2 . priority < ua . priority OR ua2 . created_at > ua . created_at )
LEFT JOIN
vpp_apps_teams vat ON vaua . adam_id = vat . adam_id AND vaua . platform = vat . platform AND vat . global_or_team_id = : global_or_team_id
2025-04-14 20:27:43 +00:00
INNER JOIN
vpp_apps ON vaua . adam_id = vpp_apps . adam_id AND vaua . platform = vpp_apps . platform
WHERE
2025-04-10 22:29:15 +00:00
-- selfServiceFilter
% s
ua . host_id = : host_id AND
ua . activity_type = ' vpp_app_install ' AND
ua2 . id IS NULL
) UNION (
-- last_vpp_install
SELECT
2025-04-14 20:27:43 +00:00
vpp_apps . title_id AS id ,
2025-04-10 22:29:15 +00:00
hvsi . command_uuid AS last_install_install_uuid ,
hvsi . created_at AS last_install_installed_at ,
hvsi . adam_id AS vpp_app_adam_id ,
2025-05-20 15:44:27 +00:00
vat . self_service AS vpp_app_self_service ,
2025-04-10 22:29:15 +00:00
-- vppAppHostStatusNamedQuery ( hvsi , ncr , status )
% s
FROM
host_vpp_software_installs hvsi
LEFT JOIN
nano_command_results ncr ON ncr . command_uuid = hvsi . command_uuid
LEFT JOIN
host_vpp_software_installs hvsi2 ON hvsi . host_id = hvsi2 . host_id AND
hvsi . adam_id = hvsi2 . adam_id AND
hvsi . platform = hvsi2 . platform AND
hvsi2 . removed = 0 AND
hvsi2 . canceled = 0 AND
( hvsi . created_at < hvsi2 . created_at OR ( hvsi . created_at = hvsi2 . created_at AND hvsi . id < hvsi2 . id ) )
2025-05-06 17:32:35 +00:00
INNER JOIN
2025-04-10 22:29:15 +00:00
vpp_apps_teams vat ON hvsi . adam_id = vat . adam_id AND hvsi . platform = vat . platform AND vat . global_or_team_id = : global_or_team_id
2025-04-14 20:27:43 +00:00
INNER JOIN
vpp_apps ON hvsi . adam_id = vpp_apps . adam_id AND hvsi . platform = vpp_apps . platform
WHERE
2025-04-10 22:29:15 +00:00
-- selfServiceFilter
% s
hvsi . host_id = : host_id AND
hvsi . removed = 0 AND
hvsi . canceled = 0 AND
hvsi2 . id IS NULL AND
NOT EXISTS (
SELECT 1
FROM
upcoming_activities ua
INNER JOIN
vpp_app_upcoming_activities vaua ON ua . id = vaua . upcoming_activity_id
WHERE
ua . host_id = hvsi . host_id AND
vaua . adam_id = hvsi . adam_id AND
vaua . platform = hvsi . platform AND
ua . activity_type = ' vpp_app_install '
)
)
` , selfServiceFilter , vppAppHostStatusNamedQuery ( "hvsi" , "ncr" , "status" ) , selfServiceFilter )
vppInstallsStmt , args , err := sqlx . Named ( vppInstallsStmt , map [ string ] any {
"host_id" : hostID ,
"global_or_team_id" : globalOrTeamID ,
"software_status_installed" : fleet . SoftwareInstalled ,
"mdm_status_acknowledged" : fleet . MDMAppleStatusAcknowledged ,
"mdm_status_error" : fleet . MDMAppleStatusError ,
"mdm_status_format_error" : fleet . MDMAppleStatusCommandFormatError ,
"software_status_failed" : fleet . SoftwareInstallFailed ,
"software_status_pending" : fleet . SoftwareInstallPending ,
} )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "build named query for host vpp installs" )
}
var vppInstalls [ ] * hostSoftware
err = sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & vppInstalls , vppInstallsStmt , args ... )
if err != nil {
return nil , err
}
2024-12-20 21:56:51 +00:00
2025-04-10 22:29:15 +00:00
return vppInstalls , nil
}
2024-12-20 21:56:51 +00:00
2025-10-28 12:33:58 +00:00
func hostInHouseInstalls ( ds * Datastore , ctx context . Context , hostID uint , globalOrTeamID uint , selfServiceOnly bool , isMDMEnrolled bool ) ( [ ] * hostSoftware , error ) {
2025-11-07 22:30:51 +00:00
var selfServiceFilter string
if selfServiceOnly {
if isMDMEnrolled {
selfServiceFilter = "(iha.self_service = 1) AND "
} else {
selfServiceFilter = "FALSE AND "
}
}
2025-10-28 12:33:58 +00:00
installsStmt := fmt . Sprintf ( `
( -- upcoming_in_house_install
SELECT
iha . title_id AS id ,
ua . execution_id AS last_install_install_uuid ,
ua . created_at AS last_install_installed_at ,
ihua . in_house_app_id AS in_house_app_id ,
2025-10-28 17:19:13 +00:00
iha . filename AS in_house_app_name ,
2025-10-28 12:33:58 +00:00
iha . platform AS in_house_app_platform ,
iha . version AS in_house_app_version ,
2025-11-07 22:30:51 +00:00
iha . self_service AS in_house_app_self_service ,
2025-10-28 12:33:58 +00:00
' pending_install ' AS status
FROM
upcoming_activities ua
INNER JOIN
in_house_app_upcoming_activities ihua ON ua . id = ihua . upcoming_activity_id
LEFT JOIN (
upcoming_activities ua2
INNER JOIN in_house_app_upcoming_activities ihua2 ON ua2 . id = ihua2 . upcoming_activity_id
) ON ua . host_id = ua2 . host_id AND
ihua . in_house_app_id = ihua2 . in_house_app_id AND
ua . activity_type = ua2 . activity_type AND
( ua2 . priority < ua . priority OR ua2 . created_at > ua . created_at )
INNER JOIN
in_house_apps iha ON ihua . in_house_app_id = iha . id
WHERE
2025-11-07 22:30:51 +00:00
-- selfServiceFilter
% s
2025-10-28 12:33:58 +00:00
ua . host_id = : host_id AND
ua . activity_type = ' in_house_app_install ' AND
iha . global_or_team_id = : global_or_team_id AND
ua2 . id IS NULL
) UNION (
-- last_in_house_install
SELECT
iha . title_id AS id ,
hihsi . command_uuid AS last_install_install_uuid ,
hihsi . created_at AS last_install_installed_at ,
hihsi . in_house_app_id AS in_house_app_id ,
2025-10-28 17:19:13 +00:00
iha . filename AS in_house_app_name ,
2025-10-28 12:33:58 +00:00
iha . platform AS in_house_app_platform ,
iha . version AS in_house_app_version ,
2025-11-07 22:30:51 +00:00
iha . self_service AS in_house_app_self_service ,
2025-10-28 12:33:58 +00:00
-- inHouseAppHostStatusNamedQuery ( hvsi , ncr , status )
% s
FROM
host_in_house_software_installs hihsi
LEFT JOIN
nano_command_results ncr ON ncr . command_uuid = hihsi . command_uuid
LEFT JOIN
host_in_house_software_installs hihsi2 ON hihsi . host_id = hihsi2 . host_id AND
hihsi . in_house_app_id = hihsi2 . in_house_app_id AND
hihsi2 . removed = 0 AND
hihsi2 . canceled = 0 AND
( hihsi . created_at < hihsi2 . created_at OR ( hihsi . created_at = hihsi2 . created_at AND hihsi . id < hihsi2 . id ) )
INNER JOIN
in_house_apps iha ON hihsi . in_house_app_id = iha . id
WHERE
2025-11-07 22:30:51 +00:00
-- selfServiceFilter
% s
2025-10-28 12:33:58 +00:00
hihsi . host_id = : host_id AND
hihsi . removed = 0 AND
hihsi . canceled = 0 AND
hihsi2 . id IS NULL AND
iha . global_or_team_id = : global_or_team_id AND
NOT EXISTS (
SELECT 1
FROM
upcoming_activities ua
INNER JOIN
in_house_app_upcoming_activities ihua ON ua . id = ihua . upcoming_activity_id
WHERE
ua . host_id = hihsi . host_id AND
ihua . in_house_app_id = hihsi . in_house_app_id AND
ua . activity_type = ' in_house_app_install '
)
)
2025-11-07 22:30:51 +00:00
` , selfServiceFilter , inHouseAppHostStatusNamedQuery ( "hihsi" , "ncr" , "status" ) , selfServiceFilter )
2025-10-28 12:33:58 +00:00
installsStmt , args , err := sqlx . Named ( installsStmt , map [ string ] any {
"host_id" : hostID ,
"global_or_team_id" : globalOrTeamID ,
"software_status_installed" : fleet . SoftwareInstalled ,
"mdm_status_acknowledged" : fleet . MDMAppleStatusAcknowledged ,
"mdm_status_error" : fleet . MDMAppleStatusError ,
"mdm_status_format_error" : fleet . MDMAppleStatusCommandFormatError ,
"software_status_failed" : fleet . SoftwareInstallFailed ,
"software_status_pending" : fleet . SoftwareInstallPending ,
} )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "build named query for host in-house installs" )
}
var installs [ ] * hostSoftware
err = sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & installs , installsStmt , args ... )
if err != nil {
return nil , err
}
return installs , nil
}
2025-05-09 19:52:11 +00:00
func pushVersion ( softwareIDStr string , softwareTitleRecord * hostSoftware , hostInstalledSoftware hostSoftware ) {
seperator := ","
if softwareTitleRecord . SoftwareIDList == nil {
softwareTitleRecord . SoftwareIDList = ptr . String ( "" )
softwareTitleRecord . SoftwareSourceList = ptr . String ( "" )
2025-10-07 21:05:22 +00:00
softwareTitleRecord . SoftwareExtensionForList = ptr . String ( "" )
2025-05-09 19:52:11 +00:00
softwareTitleRecord . VersionList = ptr . String ( "" )
softwareTitleRecord . BundleIdentifierList = ptr . String ( "" )
seperator = ""
}
softwareIDList := strings . Split ( * softwareTitleRecord . SoftwareIDList , "," )
found := false
for _ , id := range softwareIDList {
if id == softwareIDStr {
found = true
break
}
}
if ! found {
* softwareTitleRecord . SoftwareIDList += seperator + softwareIDStr
2025-05-09 20:11:27 +00:00
if hostInstalledSoftware . SoftwareSource != nil {
* softwareTitleRecord . SoftwareSourceList += seperator + * hostInstalledSoftware . SoftwareSource
}
2025-10-07 21:05:22 +00:00
if hostInstalledSoftware . SoftwareExtensionFor != nil {
* softwareTitleRecord . SoftwareExtensionForList += seperator + * hostInstalledSoftware . SoftwareExtensionFor
}
2025-05-09 19:52:11 +00:00
* softwareTitleRecord . VersionList += seperator + * hostInstalledSoftware . Version
* softwareTitleRecord . BundleIdentifierList += seperator + * hostInstalledSoftware . BundleIdentifier
}
}
2025-04-22 03:53:06 +00:00
func hostInstalledVpps ( ds * Datastore , ctx context . Context , hostID uint ) ( [ ] * hostSoftware , error ) {
vppInstalledStmt := `
SELECT
vpp_apps . title_id AS id ,
hvsi . command_uuid AS last_install_install_uuid ,
hvsi . created_at AS last_install_installed_at ,
vpp_apps . adam_id AS vpp_app_adam_id ,
vpp_apps . latest_version AS vpp_app_version ,
vpp_apps . platform as vpp_app_platform ,
NULLIF ( vpp_apps . icon_url , ' ' ) as vpp_app_icon_url ,
vpp_apps_teams . self_service AS vpp_app_self_service ,
' installed ' AS status
FROM
host_vpp_software_installs hvsi
INNER JOIN
vpp_apps ON hvsi . adam_id = vpp_apps . adam_id AND hvsi . platform = vpp_apps . platform
INNER JOIN
vpp_apps_teams ON vpp_apps . adam_id = vpp_apps_teams . adam_id AND vpp_apps . platform = vpp_apps_teams . platform
WHERE
hvsi . host_id = ?
`
var vppInstalled [ ] * hostSoftware
err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & vppInstalled , vppInstalledStmt , hostID )
if err != nil {
return nil , err
}
return vppInstalled , nil
}
2025-10-28 12:33:58 +00:00
func hostInstalledInHouses ( ds * Datastore , ctx context . Context , hostID uint ) ( [ ] * hostSoftware , error ) {
installedStmt := `
SELECT
iha . title_id AS id ,
hihsi . command_uuid AS last_install_install_uuid ,
hihsi . created_at AS last_install_installed_at ,
iha . id AS in_house_app_id ,
2025-10-28 17:19:13 +00:00
iha . filename AS in_house_app_name ,
2025-10-28 12:33:58 +00:00
iha . version AS in_house_app_version ,
iha . platform as in_house_app_platform ,
2025-11-07 22:30:51 +00:00
iha . self_service AS in_house_app_self_service ,
2025-10-28 12:33:58 +00:00
' installed ' AS status
FROM
host_in_house_software_installs hihsi
INNER JOIN
in_house_apps iha ON hihsi . in_house_app_id = iha . id
WHERE
hihsi . host_id = ?
`
var installed [ ] * hostSoftware
err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & installed , installedStmt , hostID )
if err != nil {
return nil , err
}
return installed , nil
}
2025-04-10 22:29:15 +00:00
// hydrated is the base record from the db
// it contains most of the information we need to return back, however,
// we need to copy over the install/uninstall data from the softwareTitle we fetched
// from hostSoftwareInstalls and hostSoftwareUninstalls
func hydrateHostSoftwareRecordFromDb ( hydrated * hostSoftware , softwareTitle * hostSoftware ) {
var version ,
platform string
if hydrated . PackageVersion != nil {
version = * hydrated . PackageVersion
}
if hydrated . PackagePlatform != nil {
platform = * hydrated . PackagePlatform
}
hydrated . SoftwarePackage = & fleet . SoftwarePackageOrApp {
Name : * hydrated . PackageName ,
Version : version ,
Platform : platform ,
SelfService : hydrated . PackageSelfService ,
}
2024-12-20 21:56:51 +00:00
2025-04-10 22:29:15 +00:00
// promote the last install info to the proper destination fields
if softwareTitle . LastInstallInstallUUID != nil && * softwareTitle . LastInstallInstallUUID != "" {
hydrated . SoftwarePackage . LastInstall = & fleet . HostSoftwareInstall {
InstallUUID : * softwareTitle . LastInstallInstallUUID ,
}
if softwareTitle . LastInstallInstalledAt != nil {
hydrated . SoftwarePackage . LastInstall . InstalledAt = * softwareTitle . LastInstallInstalledAt
}
}
2024-12-20 21:56:51 +00:00
2025-04-10 22:29:15 +00:00
// promote the last uninstall info to the proper destination fields
if softwareTitle . LastUninstallScriptExecutionID != nil && * softwareTitle . LastUninstallScriptExecutionID != "" {
hydrated . SoftwarePackage . LastUninstall = & fleet . HostSoftwareUninstall {
ExecutionID : * softwareTitle . LastUninstallScriptExecutionID ,
}
if softwareTitle . LastUninstallUninstalledAt != nil {
hydrated . SoftwarePackage . LastUninstall . UninstalledAt = * softwareTitle . LastUninstallUninstalledAt
}
}
}
2024-12-20 21:56:51 +00:00
2025-04-10 22:29:15 +00:00
// softwareTitleRecord is the base record, we will be modifying it
func promoteSoftwareTitleVPPApp ( softwareTitleRecord * hostSoftware ) {
var version ,
platform string
if softwareTitleRecord . VPPAppVersion != nil {
version = * softwareTitleRecord . VPPAppVersion
}
if softwareTitleRecord . VPPAppPlatform != nil {
platform = * softwareTitleRecord . VPPAppPlatform
}
softwareTitleRecord . AppStoreApp = & fleet . SoftwarePackageOrApp {
AppStoreID : * softwareTitleRecord . VPPAppAdamID ,
Version : version ,
Platform : platform ,
SelfService : softwareTitleRecord . VPPAppSelfService ,
}
2025-09-05 22:31:03 +00:00
softwareTitleRecord . IconUrl = softwareTitleRecord . VPPAppIconURL
2025-02-03 17:16:21 +00:00
2025-04-10 22:29:15 +00:00
// promote the last install info to the proper destination fields
if softwareTitleRecord . LastInstallInstallUUID != nil && * softwareTitleRecord . LastInstallInstallUUID != "" {
softwareTitleRecord . AppStoreApp . LastInstall = & fleet . HostSoftwareInstall {
CommandUUID : * softwareTitleRecord . LastInstallInstallUUID ,
}
if softwareTitleRecord . LastInstallInstalledAt != nil {
softwareTitleRecord . AppStoreApp . LastInstall . InstalledAt = * softwareTitleRecord . LastInstallInstalledAt
}
}
}
2025-02-03 17:16:21 +00:00
2025-10-28 12:33:58 +00:00
// softwareTitleRecord is the base record, we will be modifying it
func promoteSoftwareTitleInHouseApp ( softwareTitleRecord * hostSoftware ) {
var version , platform string
if softwareTitleRecord . InHouseAppVersion != nil {
version = * softwareTitleRecord . InHouseAppVersion
}
if softwareTitleRecord . InHouseAppPlatform != nil {
platform = * softwareTitleRecord . InHouseAppPlatform
}
softwareTitleRecord . SoftwarePackage = & fleet . SoftwarePackageOrApp {
Name : * softwareTitleRecord . InHouseAppName ,
Version : version ,
Platform : platform ,
2025-11-07 22:30:51 +00:00
SelfService : softwareTitleRecord . InHouseAppSelfService ,
2025-10-28 12:33:58 +00:00
}
// promote the last install info to the proper destination fields
if softwareTitleRecord . LastInstallInstallUUID != nil && * softwareTitleRecord . LastInstallInstallUUID != "" {
softwareTitleRecord . SoftwarePackage . LastInstall = & fleet . HostSoftwareInstall {
CommandUUID : * softwareTitleRecord . LastInstallInstallUUID ,
}
if softwareTitleRecord . LastInstallInstalledAt != nil {
softwareTitleRecord . SoftwarePackage . LastInstall . InstalledAt = * softwareTitleRecord . LastInstallInstalledAt
}
}
}
2025-04-10 22:29:15 +00:00
func ( ds * Datastore ) ListHostSoftware ( ctx context . Context , host * fleet . Host , opts fleet . HostSoftwareTitleListOptions ) ( [ ] * fleet . HostSoftwareWithInstaller , * fleet . PaginationMetadata , error ) {
if ! opts . VulnerableOnly && ( opts . MinimumCVSS > 0 || opts . MaximumCVSS > 0 || opts . KnownExploit ) {
return nil , nil , fleet . NewInvalidArgumentError (
"query" , "min_cvss_score, max_cvss_score, and exploit can only be provided with vulnerable=true" ,
)
}
2025-02-03 17:16:21 +00:00
2025-04-10 22:29:15 +00:00
var globalOrTeamID uint
if host . TeamID != nil {
globalOrTeamID = * host . TeamID
}
2025-07-15 20:41:42 +00:00
// By default, installer platform takes care of omitting incompatible packages, but for Linux platforms the installer
// type is a bit too broad, so we filter further here. Note that we do *not* enforce this on installations, in case
// an admin knows that e.g. alien is installed and wants to push a package anyway. We also don't check software
// inventory for deb/rpm to match that way; this differs from the logic we use for auto-install queries for rpm/deb
// packages.
incompatibleExtensions := [ ] string { "noop" }
if fleet . IsLinux ( host . Platform ) {
if ! host . PlatformSupportsDebPackages ( ) {
incompatibleExtensions = append ( incompatibleExtensions , "deb" )
}
if ! host . PlatformSupportsRpmPackages ( ) {
incompatibleExtensions = append ( incompatibleExtensions , "rpm" )
}
}
2025-04-10 22:29:15 +00:00
namedArgs := map [ string ] any {
2025-07-15 20:41:42 +00:00
"host_id" : host . ID ,
"host_platform" : host . FleetPlatform ( ) ,
"incompatible_extensions" : incompatibleExtensions ,
"global_or_team_id" : globalOrTeamID ,
"is_mdm_enrolled" : opts . IsMDMEnrolled ,
"host_label_updated_at" : host . LabelUpdatedAt ,
"avail" : opts . OnlyAvailableForInstall ,
"self_service" : opts . SelfServiceOnly ,
"min_cvss" : opts . MinimumCVSS ,
"max_cvss" : opts . MaximumCVSS ,
"vpp_apps_platforms" : fleet . VPPAppsPlatforms ,
"known_exploit" : 1 ,
2025-04-10 22:29:15 +00:00
}
2025-05-29 14:37:10 +00:00
var hasCVEMetaFilters bool
if opts . KnownExploit || opts . MinimumCVSS > 0 || opts . MaximumCVSS > 0 {
hasCVEMetaFilters = true
}
2025-04-10 22:29:15 +00:00
bySoftwareTitleID := make ( map [ uint ] * hostSoftware )
bySoftwareID := make ( map [ uint ] * hostSoftware )
2025-07-21 21:25:00 +00:00
var err error
var hostSoftwareInstallsList [ ] * hostSoftware
if opts . OnlyAvailableForInstall || opts . IncludeAvailableForInstall {
hostSoftwareInstallsList , err = hostSoftwareInstalls ( ds , ctx , host . ID )
if err != nil {
return nil , nil , err
}
for _ , s := range hostSoftwareInstallsList {
if _ , ok := bySoftwareTitleID [ s . ID ] ; ! ok {
bySoftwareTitleID [ s . ID ] = s
} else {
bySoftwareTitleID [ s . ID ] . LastInstallInstalledAt = s . LastInstallInstalledAt
bySoftwareTitleID [ s . ID ] . LastInstallInstallUUID = s . LastInstallInstallUUID
}
2025-04-10 22:29:15 +00:00
}
}
2025-02-03 17:16:21 +00:00
2025-04-10 22:29:15 +00:00
hostSoftwareUninstalls , err := hostSoftwareUninstalls ( ds , ctx , host . ID )
2025-07-01 16:08:15 +00:00
uninstallQuarantineSet := make ( map [ uint ] * hostSoftware )
2025-04-10 22:29:15 +00:00
if err != nil {
return nil , nil , err
}
for _ , s := range hostSoftwareUninstalls {
if _ , ok := bySoftwareTitleID [ s . ID ] ; ! ok {
2025-07-01 16:08:15 +00:00
if opts . OnlyAvailableForInstall || opts . IncludeAvailableForInstall {
bySoftwareTitleID [ s . ID ] = s
} else {
uninstallQuarantineSet [ s . ID ] = s
}
2025-04-10 22:29:15 +00:00
} else if bySoftwareTitleID [ s . ID ] . LastInstallInstalledAt == nil ||
( s . LastUninstallUninstalledAt != nil && s . LastUninstallUninstalledAt . After ( * bySoftwareTitleID [ s . ID ] . LastInstallInstalledAt ) ) {
2025-07-01 16:08:15 +00:00
2025-04-10 22:29:15 +00:00
// if the uninstall is more recent than the install, we should update the status
bySoftwareTitleID [ s . ID ] . Status = s . Status
bySoftwareTitleID [ s . ID ] . LastUninstallUninstalledAt = s . LastUninstallUninstalledAt
bySoftwareTitleID [ s . ID ] . LastUninstallScriptExecutionID = s . LastUninstallScriptExecutionID
bySoftwareTitleID [ s . ID ] . ExitCode = s . ExitCode
2025-07-01 16:08:15 +00:00
if ! opts . OnlyAvailableForInstall && ! opts . IncludeAvailableForInstall {
uninstallQuarantineSet [ s . ID ] = bySoftwareTitleID [ s . ID ]
delete ( bySoftwareTitleID , s . ID )
}
2025-04-10 22:29:15 +00:00
}
}
2025-02-03 17:16:21 +00:00
2025-04-10 22:29:15 +00:00
hostInstalledSoftware , err := hostInstalledSoftware ( ds , ctx , host . ID )
if err != nil {
return nil , nil , err
}
2025-10-28 12:33:58 +00:00
hostInstalledSoftwareTitleSet := make ( map [ uint ] struct { } )
hostInstalledSoftwareSet := make ( map [ uint ] * hostSoftware )
2025-08-15 14:44:01 +00:00
for _ , pointerToSoftware := range hostInstalledSoftware {
s := * pointerToSoftware
if pointerToSoftware . LastOpenedAt != nil {
timeCopy := * pointerToSoftware . LastOpenedAt
s . LastOpenedAt = & timeCopy
}
2025-07-01 16:08:15 +00:00
if unInstalled , ok := uninstallQuarantineSet [ s . ID ] ; ok {
// We have an uninstall record according to host_software_installs,
// however, osquery says the software is installed.
// Put it back into the set of returned software
bySoftwareTitleID [ s . ID ] = unInstalled
}
2025-04-10 22:29:15 +00:00
if _ , ok := bySoftwareTitleID [ s . ID ] ; ! ok {
2025-08-15 14:44:01 +00:00
sCopy := s
bySoftwareTitleID [ s . ID ] = & sCopy
} else if ( bySoftwareTitleID [ s . ID ] . LastOpenedAt == nil ) ||
( s . LastOpenedAt != nil && bySoftwareTitleID [ s . ID ] . LastOpenedAt != nil &&
s . LastOpenedAt . After ( * bySoftwareTitleID [ s . ID ] . LastOpenedAt ) ) {
existing := bySoftwareTitleID [ s . ID ]
existing . LastOpenedAt = s . LastOpenedAt
2025-04-10 22:29:15 +00:00
}
2024-12-20 21:56:51 +00:00
2025-04-22 03:53:06 +00:00
hostInstalledSoftwareTitleSet [ s . ID ] = struct { } { }
2025-04-10 22:29:15 +00:00
if s . SoftwareID != nil {
2025-08-15 14:44:01 +00:00
bySoftwareID [ * s . SoftwareID ] = pointerToSoftware
hostInstalledSoftwareSet [ * s . SoftwareID ] = pointerToSoftware
2025-04-10 22:29:15 +00:00
}
}
2024-05-01 18:37:52 +00:00
2025-04-10 22:29:15 +00:00
hostVPPInstalls , err := hostVPPInstalls ( ds , ctx , host . ID , globalOrTeamID , opts . SelfServiceOnly , opts . IsMDMEnrolled )
if err != nil {
return nil , nil , err
}
byVPPAdamID := make ( map [ string ] * hostSoftware )
for _ , s := range hostVPPInstalls {
if s . VPPAppAdamID != nil {
2025-04-14 20:27:43 +00:00
// If a VPP app is already installed on the host, we don't need to double count it
2025-05-09 20:11:27 +00:00
// until we merge the two fetch queries later on in this method.
// Until then if the host_software record is not a software installer, we delete it and keep the vpp app
2025-04-14 20:27:43 +00:00
if _ , exists := hostInstalledSoftwareTitleSet [ s . ID ] ; exists {
2025-05-06 17:32:35 +00:00
installedTitle := bySoftwareTitleID [ s . ID ]
2025-10-28 12:33:58 +00:00
if installedTitle != nil && installedTitle . InstallerID == nil {
2025-05-09 20:11:27 +00:00
// not a software installer, so copy over
// the installed title information
2025-05-06 17:32:35 +00:00
s . LastOpenedAt = installedTitle . LastOpenedAt
2025-05-09 20:11:27 +00:00
s . SoftwareID = installedTitle . SoftwareID
s . SoftwareSource = installedTitle . SoftwareSource
2025-10-07 21:05:22 +00:00
s . SoftwareExtensionFor = installedTitle . SoftwareExtensionFor
2025-05-09 20:11:27 +00:00
s . Version = installedTitle . Version
s . BundleIdentifier = installedTitle . BundleIdentifier
2025-05-29 14:37:10 +00:00
if ! opts . VulnerableOnly && ! hasCVEMetaFilters {
// When we are filtering by vulnerable only
// we want to treat the installed vpp app as a regular software title
delete ( bySoftwareTitleID , s . ID )
}
2025-07-21 21:25:00 +00:00
byVPPAdamID [ * s . VPPAppAdamID ] = s
2025-05-06 17:32:35 +00:00
} else {
continue
}
2025-07-21 21:25:00 +00:00
} else if opts . OnlyAvailableForInstall || opts . IncludeAvailableForInstall {
byVPPAdamID [ * s . VPPAppAdamID ] = s
2025-04-14 20:27:43 +00:00
}
2025-04-10 22:29:15 +00:00
}
}
2025-04-22 22:00:19 +00:00
2025-10-28 12:33:58 +00:00
hostInHouseInstalls , err := hostInHouseInstalls ( ds , ctx , host . ID , globalOrTeamID , opts . SelfServiceOnly , opts . IsMDMEnrolled )
if err != nil {
return nil , nil , err
}
byInHouseID := make ( map [ uint ] * hostSoftware )
for _ , s := range hostInHouseInstalls {
// If an in-house app is already installed on the host, we don't need to
// double count it (what does that mean? copied from the VPP comment,
// please clarify if you know) until we merge the two fetch queries later
// on in this method. Until then if the host_software record is not a
// software installer nor VPP app, we delete it and keep the in-house app.
if _ , exists := hostInstalledSoftwareTitleSet [ s . ID ] ; exists {
installedTitle := bySoftwareTitleID [ s . ID ]
if installedTitle != nil && installedTitle . InstallerID == nil {
// not a software installer, so copy over
// the installed title information
s . LastOpenedAt = installedTitle . LastOpenedAt
s . SoftwareID = installedTitle . SoftwareID
s . SoftwareSource = installedTitle . SoftwareSource
s . SoftwareExtensionFor = installedTitle . SoftwareExtensionFor
s . Version = installedTitle . Version
s . BundleIdentifier = installedTitle . BundleIdentifier
if ! opts . VulnerableOnly && ! hasCVEMetaFilters {
// When we are filtering by vulnerable only
// we want to treat the installed in-house app as a regular software title
delete ( bySoftwareTitleID , s . ID )
}
byInHouseID [ * s . InHouseAppID ] = s
} else {
continue
}
} else if opts . OnlyAvailableForInstall || opts . IncludeAvailableForInstall {
byInHouseID [ * s . InHouseAppID ] = s
}
}
2025-04-22 22:00:19 +00:00
hostInstalledVppsApps , err := hostInstalledVpps ( ds , ctx , host . ID )
2025-04-22 03:53:06 +00:00
if err != nil {
return nil , nil , err
}
2025-04-22 22:00:19 +00:00
installedVppsByAdamID := make ( map [ string ] * hostSoftware )
for _ , s := range hostInstalledVppsApps {
if s . VPPAppAdamID != nil {
installedVppsByAdamID [ * s . VPPAppAdamID ] = s
}
}
2025-05-06 17:32:35 +00:00
2025-04-22 22:00:19 +00:00
hostVPPInstalledTitles := make ( map [ uint ] * hostSoftware )
for _ , s := range installedVppsByAdamID {
2025-05-09 20:11:27 +00:00
if _ , ok := hostInstalledSoftwareTitleSet [ s . ID ] ; ok {
// we copied over all the installed title information
// from bySoftwareTitleID, but deleted the record from the map
// when going through hostVPPInstalls. Copy over the
// data from the byVPPAdamID to hostVPPInstalledTitles
// so we can later push to InstalledVersions
installedTitle := byVPPAdamID [ * s . VPPAppAdamID ]
2025-05-13 14:09:39 +00:00
if installedTitle == nil {
// This can happen when mdm_enrolled is false
// because in hostVPPInstalls we filter those out
installedTitle = bySoftwareTitleID [ s . ID ]
}
if installedTitle == nil {
// We somehow have a vpp app in host_vpp_software_installs,
// however osquery didn't pick it up in inventory
continue
}
2025-05-09 20:11:27 +00:00
s . SoftwareID = installedTitle . SoftwareID
s . SoftwareSource = installedTitle . SoftwareSource
2025-10-07 21:05:22 +00:00
s . SoftwareExtensionFor = installedTitle . SoftwareExtensionFor
2025-05-09 20:11:27 +00:00
s . Version = installedTitle . Version
s . BundleIdentifier = installedTitle . BundleIdentifier
}
2025-06-26 21:55:43 +00:00
if s . VPPAppAdamID != nil {
// Override the status; if there's a pending re-install, we should show that status.
if hs , ok := byVPPAdamID [ * s . VPPAppAdamID ] ; ok {
s . Status = hs . Status
}
}
2025-04-22 03:53:06 +00:00
hostVPPInstalledTitles [ s . ID ] = s
}
2025-04-10 22:29:15 +00:00
2025-10-28 12:33:58 +00:00
hostInstalledInHouseApps , err := hostInstalledInHouses ( ds , ctx , host . ID )
if err != nil {
return nil , nil , err
}
installedInHouseByID := make ( map [ uint ] * hostSoftware )
for _ , s := range hostInstalledInHouseApps {
if s . InHouseAppID != nil {
installedInHouseByID [ * s . InHouseAppID ] = s
}
}
hostInHouseInstalledTitles := make ( map [ uint ] * hostSoftware )
for _ , s := range installedInHouseByID {
if _ , ok := hostInstalledSoftwareTitleSet [ s . ID ] ; ok {
// we copied over all the installed title information
// from bySoftwareTitleID, but deleted the record from the map
// when going through hostInHouseInstalls. Copy over the
// data from the byInHouseID to hostInHouseInstalledTitles
// so we can later push to InstalledVersions
installedTitle := byInHouseID [ * s . InHouseAppID ]
if installedTitle == nil {
// This can happen when mdm_enrolled is false
// because in hostInHouseInstalls we filter those out
installedTitle = bySoftwareTitleID [ s . ID ]
}
if installedTitle == nil {
// We somehow have an in-house app in host_in_house_software_installs,
// however osquery didn't pick it up in inventory
continue
}
s . SoftwareID = installedTitle . SoftwareID
s . SoftwareSource = installedTitle . SoftwareSource
s . SoftwareExtensionFor = installedTitle . SoftwareExtensionFor
s . Version = installedTitle . Version
s . BundleIdentifier = installedTitle . BundleIdentifier
}
if s . InHouseAppID != nil {
// Override the status; if there's a pending re-install, we should show that status.
if hs , ok := byInHouseID [ * s . InHouseAppID ] ; ok {
s . Status = hs . Status
}
}
hostInHouseInstalledTitles [ s . ID ] = s
}
2025-04-10 22:29:15 +00:00
var stmtAvailable string
2025-04-16 14:01:34 +00:00
if opts . OnlyAvailableForInstall || opts . IncludeAvailableForInstall {
2025-04-10 22:29:15 +00:00
namedArgs [ "host_compatible_platforms" ] = host . FleetPlatform ( )
2025-04-16 14:01:34 +00:00
var availableSoftwareTitles [ ] * hostSoftware
if ! opts . VulnerableOnly {
stmtAvailable = `
SELECT
st . id ,
st . name ,
st . source ,
2025-10-07 21:05:22 +00:00
st . extension_for ,
2025-11-07 23:33:31 +00:00
st . upgrade_code ,
2025-05-06 17:32:35 +00:00
si . id as installer_id ,
2025-04-16 14:01:34 +00:00
si . self_service as package_self_service ,
si . filename as package_name ,
si . version as package_version ,
si . platform as package_platform ,
vat . self_service as vpp_app_self_service ,
vat . adam_id as vpp_app_adam_id ,
vap . latest_version as vpp_app_version ,
vap . platform as vpp_app_platform ,
NULLIF ( vap . icon_url , ' ' ) as vpp_app_icon_url ,
2025-10-28 12:33:58 +00:00
iha . id as in_house_app_id ,
2025-10-28 17:19:13 +00:00
iha . filename as in_house_app_name ,
2025-10-28 12:33:58 +00:00
iha . version as in_house_app_version ,
iha . platform as in_house_app_platform ,
2025-11-07 22:30:51 +00:00
iha . self_service as in_house_app_self_service ,
2025-04-16 14:01:34 +00:00
NULL as last_install_installed_at ,
NULL as last_install_install_uuid ,
NULL as last_uninstall_uninstalled_at ,
NULL as last_uninstall_script_execution_id ,
NULL as status
FROM
software_titles st
LEFT OUTER JOIN
-- filter out software that is not available for install on the host ' s platform
2025-07-15 20:41:42 +00:00
software_installers si ON st . id = si . title_id AND si . platform = : host_compatible_platforms AND si . extension NOT IN ( : incompatible_extensions ) AND si . global_or_team_id = : global_or_team_id
2025-04-16 14:01:34 +00:00
LEFT OUTER JOIN
-- include VPP apps only if the host is on a supported platform
vpp_apps vap ON st . id = vap . title_id AND : host_platform IN ( : vpp_apps_platforms )
LEFT OUTER JOIN
vpp_apps_teams vat ON vap . adam_id = vat . adam_id AND vap . platform = vat . platform AND vat . global_or_team_id = : global_or_team_id
2025-10-28 12:33:58 +00:00
LEFT OUTER JOIN
in_house_apps iha ON iha . title_id = st . id AND iha . platform = : host_compatible_platforms AND iha . global_or_team_id = : global_or_team_id
2025-04-16 14:01:34 +00:00
WHERE
-- software is not installed on host ( but is available in host ' s team )
NOT EXISTS (
SELECT 1
FROM
host_software hs
2025-02-11 19:53:11 +00:00
INNER JOIN
2025-04-16 14:01:34 +00:00
software s ON hs . software_id = s . id
WHERE
hs . host_id = : host_id AND
s . title_id = st . id
) AND
-- sofware install has not been attempted on host
NOT EXISTS (
SELECT 1
FROM
host_software_installs hsi
WHERE
hsi . host_id = : host_id AND
hsi . software_installer_id = si . id AND
hsi . removed = 0 AND
hsi . canceled = 0
) AND
-- sofware install / uninstall is not upcoming on host
NOT EXISTS (
SELECT 1
FROM
upcoming_activities ua
INNER JOIN
software_install_upcoming_activities siua ON siua . upcoming_activity_id = ua . id
WHERE
ua . host_id = : host_id AND
ua . activity_type IN ( ' software_install ' , ' software_uninstall ' ) AND
siua . software_installer_id = si . id
) AND
-- VPP install has not been attempted on host
NOT EXISTS (
SELECT 1
FROM
host_vpp_software_installs hvsi
WHERE
hvsi . host_id = : host_id AND
hvsi . adam_id = vat . adam_id AND
hvsi . removed = 0 AND
hvsi . canceled = 0
) AND
-- VPP install is not upcoming on host
NOT EXISTS (
SELECT 1
FROM
upcoming_activities ua
INNER JOIN
vpp_app_upcoming_activities vaua ON vaua . upcoming_activity_id = ua . id
WHERE
ua . host_id = : host_id AND
ua . activity_type = ' vpp_app_install ' AND
vaua . adam_id = vat . adam_id
) AND
2025-10-28 12:33:58 +00:00
-- in - house install has not been attempted on host
NOT EXISTS (
SELECT 1
FROM
host_in_house_software_installs hihsi
WHERE
hihsi . host_id = : host_id AND
hihsi . in_house_app_id = iha . id AND
hihsi . removed = 0 AND
hihsi . canceled = 0
) AND
-- in - house install is not upcoming on host
NOT EXISTS (
SELECT 1
FROM
upcoming_activities ua
INNER JOIN
in_house_app_upcoming_activities ihua ON ihua . upcoming_activity_id = ua . id
WHERE
ua . host_id = : host_id AND
ua . activity_type = ' in_house_app_install ' AND
ihua . in_house_app_id = iha . id
) AND
-- either the software installer or the vpp app or the in - house app exists for the host ' s team
( si . id IS NOT NULL OR vat . platform = : host_platform OR iha . id IS NOT NULL ) AND
2025-04-16 14:01:34 +00:00
-- label membership check
(
2025-10-28 12:33:58 +00:00
-- do the label membership check for software installers and VPP apps and in - house apps
2025-04-16 14:01:34 +00:00
EXISTS (
SELECT 1 FROM (
-- no labels
SELECT 0 AS count_installer_labels , 0 AS count_host_labels , 0 as count_host_updated_after_labels
2025-10-28 12:33:58 +00:00
WHERE
NOT EXISTS ( SELECT 1 FROM software_installer_labels sil WHERE sil . software_installer_id = si . id ) AND
NOT EXISTS ( SELECT 1 FROM vpp_app_team_labels vatl WHERE vatl . vpp_app_team_id = vat . id ) AND
NOT EXISTS ( SELECT 1 FROM in_house_app_labels ihl WHERE ihl . in_house_app_id = iha . id )
2025-04-16 14:01:34 +00:00
UNION
2025-10-28 12:33:58 +00:00
-- include any for software installers
2025-04-16 14:01:34 +00:00
SELECT
COUNT ( * ) AS count_installer_labels ,
COUNT ( lm . label_id ) AS count_host_labels ,
0 as count_host_updated_after_labels
FROM
software_installer_labels sil
LEFT OUTER JOIN label_membership lm ON lm . label_id = sil . label_id
AND lm . host_id = : host_id
WHERE
sil . software_installer_id = si . id
AND sil . exclude = 0
HAVING
count_installer_labels > 0 AND count_host_labels > 0
UNION
2025-10-28 12:33:58 +00:00
-- exclude any for software installers , ignore software that depends on labels created
2025-04-16 14:01:34 +00:00
-- _after_ the label_updated_at timestamp of the host ( because
-- we don ' t have results for that label yet , the host may or may
-- not be a member ) .
SELECT
COUNT ( * ) AS count_installer_labels ,
COUNT ( lm . label_id ) AS count_host_labels ,
SUM ( CASE WHEN lbl . created_at IS NOT NULL AND : host_label_updated_at >= lbl . created_at THEN 1 ELSE 0 END ) as count_host_updated_after_labels
FROM
software_installer_labels sil
LEFT OUTER JOIN labels lbl
ON lbl . id = sil . label_id
LEFT OUTER JOIN label_membership lm
ON lm . label_id = sil . label_id AND lm . host_id = : host_id
WHERE
sil . software_installer_id = si . id
AND sil . exclude = 1
HAVING
count_installer_labels > 0 AND count_installer_labels = count_host_updated_after_labels AND count_host_labels = 0
UNION
2025-10-28 12:33:58 +00:00
-- include any for VPP apps
2025-04-16 14:01:34 +00:00
SELECT
COUNT ( * ) AS count_installer_labels ,
COUNT ( lm . label_id ) AS count_host_labels ,
0 as count_host_updated_after_labels
FROM
vpp_app_team_labels vatl
LEFT OUTER JOIN label_membership lm ON lm . label_id = vatl . label_id
AND lm . host_id = : host_id
WHERE
vatl . vpp_app_team_id = vat . id
AND vatl . exclude = 0
HAVING
count_installer_labels > 0 AND count_host_labels > 0
UNION
2025-10-28 12:33:58 +00:00
-- exclude any for VPP apps
2025-04-16 14:01:34 +00:00
SELECT
COUNT ( * ) AS count_installer_labels ,
COUNT ( lm . label_id ) AS count_host_labels ,
SUM ( CASE
WHEN lbl . created_at IS NOT NULL AND lbl . label_membership_type = 0 AND : host_label_updated_at >= lbl . created_at THEN 1
WHEN lbl . created_at IS NOT NULL AND lbl . label_membership_type = 1 THEN 1
ELSE 0 END ) as count_host_updated_after_labels
FROM
vpp_app_team_labels vatl
LEFT OUTER JOIN labels lbl
ON lbl . id = vatl . label_id
LEFT OUTER JOIN label_membership lm
ON lm . label_id = vatl . label_id AND lm . host_id = : host_id
WHERE
vatl . vpp_app_team_id = vat . id
AND vatl . exclude = 1
HAVING
count_installer_labels > 0 AND count_installer_labels = count_host_updated_after_labels AND count_host_labels = 0
2025-10-28 12:33:58 +00:00
UNION
-- include any for in - house apps
SELECT
COUNT ( * ) AS count_installer_labels ,
COUNT ( lm . label_id ) AS count_host_labels ,
0 as count_host_updated_after_labels
FROM
in_house_app_labels ihl
LEFT OUTER JOIN label_membership lm ON lm . label_id = ihl . label_id AND lm . host_id = : host_id
WHERE
ihl . in_house_app_id = iha . id
AND ihl . exclude = 0
HAVING
count_installer_labels > 0 AND count_host_labels > 0
UNION
-- exclude any for in - house apps
SELECT
COUNT ( * ) AS count_installer_labels ,
COUNT ( lm . label_id ) AS count_host_labels ,
SUM ( CASE
WHEN lbl . created_at IS NOT NULL AND lbl . label_membership_type = 0 AND : host_label_updated_at >= lbl . created_at THEN 1
WHEN lbl . created_at IS NOT NULL AND lbl . label_membership_type = 1 THEN 1
ELSE 0 END ) as count_host_updated_after_labels
FROM
in_house_app_labels ihl
LEFT OUTER JOIN labels lbl ON lbl . id = ihl . label_id
LEFT OUTER JOIN label_membership lm ON lm . label_id = ihl . label_id AND lm . host_id = : host_id
WHERE
ihl . in_house_app_id = iha . id AND
ihl . exclude = 1
HAVING
count_installer_labels > 0 AND count_installer_labels = count_host_updated_after_labels AND count_host_labels = 0
2025-04-16 14:01:34 +00:00
) t
)
)
`
if opts . SelfServiceOnly {
2025-11-07 22:30:51 +00:00
stmtAvailable += "\nAND ( si.self_service = 1 OR ( vat.self_service = 1 AND :is_mdm_enrolled ) OR ( iha.self_service = 1 AND :is_mdm_enrolled ) )"
2025-04-16 14:01:34 +00:00
}
2024-05-01 18:37:52 +00:00
2025-04-16 14:01:34 +00:00
if ! opts . IsMDMEnrolled {
2025-10-28 12:33:58 +00:00
// both VPP apps and in-house apps require MDM
stmtAvailable += "\nAND vat.id IS NULL AND iha.id IS NULL"
2025-04-16 14:01:34 +00:00
}
2024-05-07 15:28:16 +00:00
2025-04-16 14:01:34 +00:00
stmtAvailable , args , err := sqlx . Named ( stmtAvailable , namedArgs )
if err != nil {
return nil , nil , err
}
stmtAvailable , args , err = sqlx . In ( stmtAvailable , args ... )
if err != nil {
return nil , nil , err
}
2024-07-16 20:18:44 +00:00
2025-04-16 14:01:34 +00:00
err = sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & availableSoftwareTitles , stmtAvailable , args ... )
if err != nil {
return nil , nil , err
}
2025-04-10 22:29:15 +00:00
}
// These slices are meant to keep track of software that is available for install.
// When we are filtering by `OnlyAvailableForInstall`, we will replace the existing
2025-10-28 12:33:58 +00:00
// software title records held in bySoftwareTitleID, byVPPAdamID and byInHouseID.
2025-04-10 22:29:15 +00:00
// If we are just using the `IncludeAvailableForInstall` options, we will simply
2025-10-28 12:33:58 +00:00
// add these addtional software titles to bySoftwareTitleID, byVPPAdamID and byInHouseID.
tmpBySoftwareTitleID := make ( map [ uint ] * hostSoftware , len ( availableSoftwareTitles ) )
2025-04-10 22:29:15 +00:00
tmpByVPPAdamID := make ( map [ string ] * hostSoftware , len ( byVPPAdamID ) )
2025-10-28 12:33:58 +00:00
tmpByInHouseID := make ( map [ uint ] * hostSoftware , len ( byInHouseID ) )
2025-04-10 22:29:15 +00:00
if opts . OnlyAvailableForInstall {
// drop in anything that has been installed or uninstalled as it can be installed again regardless of status
for _ , s := range hostSoftwareUninstalls {
2025-10-28 12:33:58 +00:00
tmpBySoftwareTitleID [ s . ID ] = s
2025-04-10 22:29:15 +00:00
}
2025-04-16 14:01:34 +00:00
if ! opts . VulnerableOnly {
2025-07-21 21:25:00 +00:00
for _ , s := range hostSoftwareInstallsList {
2025-10-28 12:33:58 +00:00
tmpBySoftwareTitleID [ s . ID ] = s
2025-04-16 14:01:34 +00:00
}
for _ , s := range hostVPPInstalls {
tmpByVPPAdamID [ * s . VPPAppAdamID ] = s
}
2025-10-28 12:33:58 +00:00
for _ , s := range hostInHouseInstalls {
tmpByInHouseID [ * s . InHouseAppID ] = s
}
2025-04-10 22:29:15 +00:00
}
2025-05-06 17:32:35 +00:00
}
2025-10-28 12:33:58 +00:00
// NOTE: label conditions are applied in a subsequent step
2025-05-06 17:32:35 +00:00
// software installed on the host not by fleet and there exists a software installer that matches this software
// so that makes it available for install
installedInstallersSql := `
2025-04-10 22:29:15 +00:00
SELECT
2025-05-06 17:32:35 +00:00
software . title_id ,
2025-05-13 23:41:33 +00:00
software_installers . id AS installer_id ,
software_installers . self_service AS package_self_service
2025-04-10 22:29:15 +00:00
FROM
host_software
INNER JOIN
software ON host_software . software_id = software . id
INNER JOIN
software_installers ON software . title_id = software_installers . title_id
AND software_installers . platform = ?
AND software_installers . global_or_team_id = ?
WHERE host_software . host_id = ?
`
2025-05-06 17:32:35 +00:00
type InstalledSoftwareTitle struct {
2025-05-13 23:41:33 +00:00
TitleID uint ` db:"title_id" `
InstallerID uint ` db:"installer_id" `
SelfService bool ` db:"package_self_service" `
2025-05-06 17:32:35 +00:00
}
var installedSoftwareTitleIDs [ ] InstalledSoftwareTitle
err = sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & installedSoftwareTitleIDs , installedInstallersSql , namedArgs [ "host_compatible_platforms" ] , globalOrTeamID , host . ID )
if err != nil {
return nil , nil , err
}
for _ , s := range installedSoftwareTitleIDs {
if software := bySoftwareTitleID [ s . TitleID ] ; software != nil {
software . InstallerID = & s . InstallerID
2025-05-13 23:41:33 +00:00
software . PackageSelfService = & s . SelfService
2025-10-28 12:33:58 +00:00
tmpBySoftwareTitleID [ s . TitleID ] = software
2025-04-10 22:29:15 +00:00
}
2025-05-06 17:32:35 +00:00
}
2025-05-13 14:09:39 +00:00
if ! opts . SelfServiceOnly || ( opts . SelfServiceOnly && opts . IsMDMEnrolled ) {
2025-10-28 12:33:58 +00:00
// NOTE: label conditions are applied in a subsequent step
2025-05-13 14:09:39 +00:00
// software installed on the host not by fleet and there exists a vpp app that matches this software
// so that makes it available for install
installedVPPAppsSql := `
SELECT
vpp_apps . title_id AS id ,
vpp_apps . adam_id AS vpp_app_adam_id ,
vpp_apps . latest_version AS vpp_app_version ,
vpp_apps . platform as vpp_app_platform ,
NULLIF ( vpp_apps . icon_url , ' ' ) as vpp_app_icon_url ,
vpp_apps_teams . self_service AS vpp_app_self_service
FROM
host_software
INNER JOIN
software ON host_software . software_id = software . id
INNER JOIN
vpp_apps ON software . title_id = vpp_apps . title_id AND : host_platform IN ( : vpp_apps_platforms )
INNER JOIN
vpp_apps_teams ON vpp_apps . adam_id = vpp_apps_teams . adam_id AND vpp_apps . platform = vpp_apps_teams . platform AND vpp_apps_teams . global_or_team_id = : global_or_team_id
WHERE
host_software . host_id = : host_id
`
installedVPPAppsSql , args , err := sqlx . Named ( installedVPPAppsSql , namedArgs )
if err != nil {
return nil , nil , err
2025-05-09 19:52:11 +00:00
}
2025-05-13 14:09:39 +00:00
installedVPPAppsSql , args , err = sqlx . In ( installedVPPAppsSql , args ... )
if err != nil {
return nil , nil , err
2025-05-13 03:20:33 +00:00
}
2025-05-13 14:09:39 +00:00
var installedVPPAppIDs [ ] * hostSoftware
err = sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & installedVPPAppIDs , installedVPPAppsSql , args ... )
if err != nil {
return nil , nil , err
}
for _ , s := range installedVPPAppIDs {
if s . VPPAppAdamID != nil {
2025-08-01 18:39:51 +00:00
if tmpByVPPAdamID [ * s . VPPAppAdamID ] == nil {
// inventoried by osquery, but not installed by fleet
tmpByVPPAdamID [ * s . VPPAppAdamID ] = s
} else {
// inventoried by osquery, but installed by fleet
// We want to preserve the install information from host_vpp_software_installs
// so don't overwrite the existing record
tmpByVPPAdamID [ * s . VPPAppAdamID ] . VPPAppVersion = s . VPPAppVersion
tmpByVPPAdamID [ * s . VPPAppAdamID ] . VPPAppPlatform = s . VPPAppPlatform
tmpByVPPAdamID [ * s . VPPAppAdamID ] . VPPAppIconURL = s . VPPAppIconURL
}
2025-05-13 14:09:39 +00:00
}
if VPPAppByFleet , ok := hostVPPInstalledTitles [ s . ID ] ; ok {
// Vpp app installed by fleet, so we need to copy over the status,
// because all fleet installed apps show an installed status if available
tmpByVPPAdamID [ * s . VPPAppAdamID ] . Status = VPPAppByFleet . Status
}
// If a VPP app is installed on the host, but not by fleet
// it will be present in bySoftwareTitleID, because osquery returned it as inventory.
// We need to remove it from bySoftwareTitleID and add it to byVPPAdamID
2025-10-28 12:33:58 +00:00
if inventoriedSoftware , ok := bySoftwareTitleID [ s . ID ] ; ok {
inventoriedSoftware . VPPAppAdamID = s . VPPAppAdamID
inventoriedSoftware . VPPAppVersion = s . VPPAppVersion
inventoriedSoftware . VPPAppPlatform = s . VPPAppPlatform
inventoriedSoftware . VPPAppIconURL = s . VPPAppIconURL
inventoriedSoftware . VPPAppSelfService = s . VPPAppSelfService
2025-05-29 14:37:10 +00:00
if ! opts . VulnerableOnly && ! hasCVEMetaFilters {
// When we are filtering by vulnerable only
// we want to treat the installed vpp app as a regular software title
delete ( bySoftwareTitleID , s . ID )
2025-10-28 12:33:58 +00:00
byVPPAdamID [ * s . VPPAppAdamID ] = inventoriedSoftware
2025-05-29 14:37:10 +00:00
}
2025-10-28 12:33:58 +00:00
hostVPPInstalledTitles [ s . ID ] = inventoriedSoftware
}
}
// NOTE: label conditions are applied in a subsequent step
// software installed on the host not by fleet and there exists an
// in-house app that matches this software so that makes it available for
// install
installedInHouseAppsSql := `
SELECT
iha . title_id AS id ,
iha . id AS in_house_app_id ,
2025-10-28 17:19:13 +00:00
iha . filename AS in_house_app_name ,
2025-10-28 12:33:58 +00:00
iha . version AS in_house_app_version ,
2025-11-07 22:30:51 +00:00
iha . platform as in_house_app_platform ,
iha . self_service AS in_house_app_self_service
2025-10-28 12:33:58 +00:00
FROM
host_software
INNER JOIN
software ON host_software . software_id = software . id
INNER JOIN
in_house_apps iha ON software . title_id = iha . title_id AND iha . global_or_team_id = : global_or_team_id AND iha . platform = : host_compatible_platforms
WHERE
host_software . host_id = : host_id
`
installedInHouseAppsSql , args , err = sqlx . Named ( installedInHouseAppsSql , namedArgs )
if err != nil {
return nil , nil , err
}
installedInHouseAppsSql , args , err = sqlx . In ( installedInHouseAppsSql , args ... )
if err != nil {
return nil , nil , err
}
var installedInHouseAppIDs [ ] * hostSoftware
err = sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & installedInHouseAppIDs , installedInHouseAppsSql , args ... )
if err != nil {
return nil , nil , err
}
for _ , s := range installedInHouseAppIDs {
if s . InHouseAppID != nil {
if tmpByInHouseID [ * s . InHouseAppID ] == nil {
// inventoried, but not installed by fleet
tmpByInHouseID [ * s . InHouseAppID ] = s
} else {
// inventoried, but installed by fleet
// We want to preserve the install information from host_in_house_software_installs
// so don't overwrite the existing record
tmpByInHouseID [ * s . InHouseAppID ] . InHouseAppVersion = s . InHouseAppVersion
tmpByInHouseID [ * s . InHouseAppID ] . InHouseAppPlatform = s . InHouseAppPlatform
tmpByInHouseID [ * s . InHouseAppID ] . InHouseAppName = s . InHouseAppName
}
}
if inHouseAppByFleet , ok := hostInHouseInstalledTitles [ s . ID ] ; ok {
// In-house app installed by fleet, so we need to copy over the status,
// because all fleet installed apps show an installed status if available
tmpByInHouseID [ * s . InHouseAppID ] . Status = inHouseAppByFleet . Status
}
// If an in-house app is installed on the host, but not by fleet
// it will be present in bySoftwareTitleID, because osquery returned it as inventory.
// We need to remove it from bySoftwareTitleID and add it to byInHouseID
if inventoriedSoftware , ok := bySoftwareTitleID [ s . ID ] ; ok {
inventoriedSoftware . InHouseAppID = s . InHouseAppID
inventoriedSoftware . InHouseAppVersion = s . InHouseAppVersion
inventoriedSoftware . InHouseAppPlatform = s . InHouseAppPlatform
inventoriedSoftware . InHouseAppName = s . InHouseAppName
2025-11-07 22:30:51 +00:00
inventoriedSoftware . InHouseAppSelfService = s . InHouseAppSelfService
2025-10-28 12:33:58 +00:00
if ! opts . VulnerableOnly && ! hasCVEMetaFilters {
// When we are filtering by vulnerable only
// we want to treat the installed in-house app as a regular software title
delete ( bySoftwareTitleID , s . ID )
byInHouseID [ * s . InHouseAppID ] = inventoriedSoftware
}
hostInHouseInstalledTitles [ s . ID ] = inventoriedSoftware
2025-05-13 14:09:39 +00:00
}
2025-05-09 19:52:11 +00:00
}
2025-04-10 22:29:15 +00:00
}
2025-05-13 14:09:39 +00:00
2025-04-10 22:29:15 +00:00
for _ , s := range availableSoftwareTitles {
2025-10-28 12:33:58 +00:00
switch {
case s . VPPAppAdamID != nil :
// VPP app
2025-04-10 22:29:15 +00:00
existingVPP , found := byVPPAdamID [ * s . VPPAppAdamID ]
if opts . OnlyAvailableForInstall {
if ! found {
tmpByVPPAdamID [ * s . VPPAppAdamID ] = s
} else {
tmpByVPPAdamID [ * s . VPPAppAdamID ] = existingVPP
}
} else {
// We have an existing vpp record in an installed or pending state, do not overwrite with the
// one that's available for install. We would lose specifics about the installed version
if ! found {
byVPPAdamID [ * s . VPPAppAdamID ] = s
}
}
2024-05-16 22:09:41 +00:00
2025-10-28 12:33:58 +00:00
case s . InHouseAppID != nil :
// In-house app
existing , found := byInHouseID [ * s . InHouseAppID ]
if opts . OnlyAvailableForInstall {
if ! found {
tmpByInHouseID [ * s . InHouseAppID ] = s
} else {
tmpByInHouseID [ * s . InHouseAppID ] = existing
}
} else {
// We have an existing in-house record in an installed or pending state, do not overwrite with the
// one that's available for install. We would lose specifics about the installed version
if ! found {
byInHouseID [ * s . InHouseAppID ] = s
}
}
2024-05-01 18:37:52 +00:00
2025-10-28 12:33:58 +00:00
default :
existingSoftware , found := bySoftwareTitleID [ s . ID ]
2025-04-10 22:29:15 +00:00
if opts . OnlyAvailableForInstall {
if ! found {
2025-10-28 12:33:58 +00:00
tmpBySoftwareTitleID [ s . ID ] = s
2025-04-10 22:29:15 +00:00
} else {
2025-10-28 12:33:58 +00:00
tmpBySoftwareTitleID [ s . ID ] = existingSoftware
2025-04-10 22:29:15 +00:00
}
} else {
// We have an existing software record in an installed or pending state, do not overwrite with the
// one that's available for install. We would lose specifics about the previous record
if ! found {
bySoftwareTitleID [ s . ID ] = s
}
}
}
}
// Clear out all the previous software titles as we are only filtering for available software
if opts . OnlyAvailableForInstall {
2025-10-28 12:33:58 +00:00
bySoftwareTitleID = tmpBySoftwareTitleID
2025-04-10 22:29:15 +00:00
byVPPAdamID = tmpByVPPAdamID
2025-10-28 12:33:58 +00:00
byInHouseID = tmpByInHouseID
2025-04-10 22:29:15 +00:00
}
}
2024-05-01 18:37:52 +00:00
2025-05-06 17:32:35 +00:00
// filter out software installers due to label scoping
filteredBySoftwareTitleID , err := filterSoftwareInstallersByLabel (
ds ,
ctx ,
host ,
bySoftwareTitleID ,
)
if err != nil {
return nil , nil , err
}
2025-10-28 12:33:58 +00:00
// filter out VPP apps due to label scoping
filteredByVPPAdamID , otherVppAppsInInventory , err := filterVPPAppsByLabel (
2025-05-06 17:32:35 +00:00
ds ,
ctx ,
host ,
byVPPAdamID ,
hostVPPInstalledTitles ,
)
if err != nil {
return nil , nil , err
}
2025-10-28 12:33:58 +00:00
// filter out in-house apps due to label scoping
filteredByInHouseID , otherInHouseAppsInInventory , err := filterInHouseAppsByLabel (
ds ,
ctx ,
host ,
byInHouseID ,
hostInHouseInstalledTitles ,
)
if err != nil {
return nil , nil , err
}
2025-05-06 17:32:35 +00:00
// We ignored the VPP apps that were installed on the host while filtering in filterSoftwareInstallersByLabel
// so we need to add them back in if they are allowed by filterVppAppsByLabel
for _ , value := range otherVppAppsInInventory {
if st , ok := bySoftwareTitleID [ value . ID ] ; ok {
filteredBySoftwareTitleID [ value . ID ] = st
}
}
2025-10-28 12:33:58 +00:00
// We ignored the in-house apps that were installed on the host while filtering in filterSoftwareInstallersByLabel
// so we need to add them back in if they are allowed by filterInHouseAppsByLabel
for _ , value := range otherInHouseAppsInInventory {
if st , ok := bySoftwareTitleID [ value . ID ] ; ok {
filteredBySoftwareTitleID [ value . ID ] = st
}
}
2025-05-06 17:32:35 +00:00
if opts . OnlyAvailableForInstall {
bySoftwareTitleID = filteredBySoftwareTitleID
byVPPAdamID = filteredByVPPAdamID
2025-10-28 12:33:58 +00:00
byInHouseID = filteredByInHouseID
2025-05-06 17:32:35 +00:00
}
// self service impacts inventory, when a software title is excluded because of a filter,
// it should be excluded from the inventory as well, because we cannot "reinstall" it on the self service page
if opts . SelfServiceOnly {
for _ , software := range bySoftwareTitleID {
if software . PackageSelfService != nil && * software . PackageSelfService {
if filteredBySoftwareTitleID [ software . ID ] == nil {
// remove the software title from bySoftwareTitleID
delete ( bySoftwareTitleID , software . ID )
}
}
}
2025-05-13 23:41:33 +00:00
for vppAppAdamID , software := range byVPPAdamID {
if software . VPPAppSelfService != nil && * software . VPPAppSelfService {
if filteredByVPPAdamID [ vppAppAdamID ] == nil {
// remove the software title from byVPPAdamID
delete ( byVPPAdamID , vppAppAdamID )
}
}
}
2025-11-07 22:30:51 +00:00
for inHouseID , software := range byInHouseID {
if software . InHouseAppSelfService != nil && * software . InHouseAppSelfService {
if filteredByInHouseID [ inHouseID ] == nil {
// remove the software title from byInHouseID
delete ( byInHouseID , inHouseID )
}
}
}
2025-05-06 17:32:35 +00:00
}
2025-10-28 12:33:58 +00:00
// since these host installed vpp apps/in-house apps are already added in bySoftwareTitleID,
// we need to avoid adding them to byVPPAdamID/byInHouseID
// but we need to store them in filteredBy{VPPAdamID,InHouseID} so they are able to be
2025-05-06 17:32:35 +00:00
// promoted when returning the software title
for key , value := range otherVppAppsInInventory {
if _ , ok := filteredByVPPAdamID [ key ] ; ! ok {
filteredByVPPAdamID [ key ] = value
}
}
2025-10-28 12:33:58 +00:00
for key , value := range otherInHouseAppsInInventory {
if _ , ok := filteredByInHouseID [ key ] ; ! ok {
filteredByInHouseID [ key ] = value
}
}
2025-05-06 17:32:35 +00:00
2025-10-28 12:33:58 +00:00
var softwareTitleIDs [ ] uint
2025-04-10 22:29:15 +00:00
for softwareTitleID := range bySoftwareTitleID {
2025-10-28 12:33:58 +00:00
softwareTitleIDs = append ( softwareTitleIDs , softwareTitleID )
2024-05-01 18:37:52 +00:00
}
2025-04-10 22:29:15 +00:00
var softwareIDs [ ] uint
for softwareID := range bySoftwareID {
softwareIDs = append ( softwareIDs , softwareID )
2024-05-01 18:37:52 +00:00
}
2025-04-10 22:29:15 +00:00
var vppAdamIDs [ ] string
2025-10-28 12:33:58 +00:00
var vppTitleIDs [ ] uint
2025-08-01 15:22:14 +00:00
for key , v := range byVPPAdamID {
2025-04-10 22:29:15 +00:00
vppAdamIDs = append ( vppAdamIDs , key )
2025-10-28 12:33:58 +00:00
vppTitleIDs = append ( vppTitleIDs , v . ID )
}
var inHouseIDs [ ] uint
var inHouseTitleIDs [ ] uint
for key , v := range byInHouseID {
inHouseIDs = append ( inHouseIDs , key )
inHouseTitleIDs = append ( inHouseTitleIDs , v . ID )
2025-04-10 22:29:15 +00:00
}
2024-05-29 12:54:48 +00:00
2025-04-10 22:29:15 +00:00
var titleCount uint
var hostSoftwareList [ ] * hostSoftware
2025-10-28 12:33:58 +00:00
if len ( softwareTitleIDs ) > 0 || len ( vppAdamIDs ) > 0 || len ( inHouseIDs ) > 0 {
var (
args [ ] interface { }
stmt string
softwareTitleStatement string
vppAdamStatment string
)
2025-04-10 22:29:15 +00:00
matchClause := ""
matchArgs := [ ] interface { } { }
if opts . ListOptions . MatchQuery != "" {
matchClause , matchArgs = searchLike ( matchClause , matchArgs , opts . ListOptions . MatchQuery , "software_titles.name" )
}
2025-11-07 22:30:51 +00:00
var (
softwareOnlySelfServiceClause string
vppOnlySelfServiceClause string
inHouseOnlySelfServiceClause string
)
2025-04-10 22:29:15 +00:00
if opts . SelfServiceOnly {
softwareOnlySelfServiceClause = ` AND software_installers.self_service = 1 `
if opts . IsMDMEnrolled {
2025-05-09 19:52:11 +00:00
vppOnlySelfServiceClause = ` AND vpp_apps_teams.self_service = 1 `
2025-11-07 22:30:51 +00:00
inHouseOnlySelfServiceClause = ` AND in_house_apps.self_service = 1 `
2024-05-29 12:54:48 +00:00
}
2025-04-10 22:29:15 +00:00
}
2024-07-24 17:39:23 +00:00
2025-04-10 22:29:15 +00:00
var cveMetaFilter string
var cveMatchClause string
var cveNamedArgs [ ] interface { }
var cveMatchArgs [ ] interface { }
if opts . KnownExploit {
cveMetaFilter += "\nAND cve_meta.cisa_known_exploit = :known_exploit"
}
if opts . MinimumCVSS > 0 {
cveMetaFilter += "\nAND cve_meta.cvss_score >= :min_cvss"
}
if opts . MaximumCVSS > 0 {
cveMetaFilter += "\nAND cve_meta.cvss_score <= :max_cvss"
}
if hasCVEMetaFilters {
cveMetaFilter , cveNamedArgs , err = sqlx . Named ( cveMetaFilter , namedArgs )
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "build named query for cve meta filters" )
}
}
if opts . ListOptions . MatchQuery != "" {
cveMatchClause , cveMatchArgs = searchLike ( cveMatchClause , cveMatchArgs , opts . ListOptions . MatchQuery , "software_cve.cve" )
}
var softwareVulnerableJoin string
2025-10-28 12:33:58 +00:00
if len ( softwareTitleIDs ) > 0 {
2025-04-10 22:29:15 +00:00
if opts . VulnerableOnly || opts . ListOptions . MatchQuery != "" {
softwareVulnerableJoin += " AND ( "
if ! opts . VulnerableOnly && opts . ListOptions . MatchQuery != "" {
softwareVulnerableJoin += `
2025-04-14 20:48:59 +00:00
-- Software without vulnerabilities
(
NOT EXISTS (
SELECT 1
FROM
software_cve
WHERE
software_cve . software_id = software . id
) ` + matchClause + `
) OR
2025-04-10 22:29:15 +00:00
`
2024-07-24 17:39:23 +00:00
}
2025-04-10 22:29:15 +00:00
softwareVulnerableJoin += `
2025-04-14 20:48:59 +00:00
-- Software with vulnerabilities
2025-04-10 22:29:15 +00:00
EXISTS (
SELECT 1
FROM
software_cve
`
cveMetaJoin := "\n INNER JOIN cve_meta ON software_cve.cve = cve_meta.cve"
// Only join CVE table if there are filters
if hasCVEMetaFilters {
softwareVulnerableJoin += cveMetaJoin
2024-07-24 17:39:23 +00:00
}
2025-04-10 22:29:15 +00:00
softwareVulnerableJoin += `
WHERE
software_cve . software_id = software . id
`
softwareVulnerableJoin += cveMetaFilter
2025-04-14 20:48:59 +00:00
softwareVulnerableJoin += "\n" + strings . ReplaceAll ( cveMatchClause , "AND" , "AND (" )
softwareVulnerableJoin += strings . ReplaceAll ( matchClause , "AND" , "OR" ) + ")"
softwareVulnerableJoin += "\n)"
2025-04-16 14:02:24 +00:00
if ! opts . VulnerableOnly || opts . ListOptions . MatchQuery != "" {
2025-04-14 20:48:59 +00:00
softwareVulnerableJoin += ")"
}
2024-07-24 17:39:23 +00:00
}
2024-09-05 19:20:36 +00:00
2025-04-10 22:29:15 +00:00
installedSoftwareJoinsCondition := ""
if len ( softwareIDs ) > 0 {
installedSoftwareJoinsCondition = ` AND software.id IN (?) `
2024-09-05 19:20:36 +00:00
}
2024-05-29 12:54:48 +00:00
2025-04-10 22:29:15 +00:00
softwareTitleStatement = `
-- SELECT for software
% s
FROM
software_titles
LEFT JOIN
2025-08-14 14:13:37 +00:00
software_installers ON software_titles . id = software_installers . title_id
2025-04-10 22:29:15 +00:00
AND software_installers . global_or_team_id = : global_or_team_id
LEFT JOIN
software ON software_titles . id = software . title_id ` + installedSoftwareJoinsCondition + `
WHERE
software_titles . id IN ( ? )
% s
` + softwareOnlySelfServiceClause + `
-- GROUP by for software
% s
`
var softwareTitleArgs [ ] interface { }
if len ( softwareIDs ) > 0 {
2025-10-28 12:33:58 +00:00
softwareTitleStatement , softwareTitleArgs , err = sqlx . In ( softwareTitleStatement , softwareIDs , softwareTitleIDs )
2025-04-10 22:29:15 +00:00
} else {
2025-10-28 12:33:58 +00:00
softwareTitleStatement , softwareTitleArgs , err = sqlx . In ( softwareTitleStatement , softwareTitleIDs )
2024-07-16 20:52:04 +00:00
}
2025-04-10 22:29:15 +00:00
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "expand IN query for software titles" )
}
softwareTitleStatement , softwareTitleArgsNamedArgs , err := sqlx . Named ( softwareTitleStatement , namedArgs )
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "build named query for software titles" )
2025-03-17 13:59:03 +00:00
}
2025-04-10 22:29:15 +00:00
args = append ( args , softwareTitleArgsNamedArgs ... )
args = append ( args , softwareTitleArgs ... )
if len ( cveNamedArgs ) > 0 {
args = append ( args , cveNamedArgs ... )
2024-07-16 20:52:04 +00:00
}
2025-04-10 22:29:15 +00:00
if len ( cveMatchArgs ) > 0 {
args = append ( args , cveMatchArgs ... )
}
if len ( matchArgs ) > 0 {
args = append ( args , matchArgs ... )
2025-04-16 14:02:24 +00:00
// Have to conditionally add the additional match for software without vulnerabilities
if ! opts . VulnerableOnly && opts . ListOptions . MatchQuery != "" {
args = append ( args , matchArgs ... )
}
2025-04-10 22:29:15 +00:00
}
stmt += softwareTitleStatement
}
2024-07-24 17:39:23 +00:00
2025-04-10 22:29:15 +00:00
if ! opts . VulnerableOnly && len ( vppAdamIDs ) > 0 {
2025-10-28 12:33:58 +00:00
if len ( softwareTitleIDs ) > 0 {
2025-04-10 22:29:15 +00:00
vppAdamStatment = ` UNION `
}
vppAdamStatment += `
-- SELECT for vpp apps
% s
FROM
software_titles
INNER JOIN
vpp_apps ON software_titles . id = vpp_apps . title_id AND vpp_apps . platform = : host_platform
INNER JOIN
vpp_apps_teams ON vpp_apps . adam_id = vpp_apps_teams . adam_id AND vpp_apps . platform = vpp_apps_teams . platform AND vpp_apps_teams . global_or_team_id = : global_or_team_id
WHERE
vpp_apps . adam_id IN ( ? )
AND true
` + vppOnlySelfServiceClause + `
-- GROUP BY for vpp apps
% s
`
vppAdamStatement , vppAdamArgs , err := sqlx . In ( vppAdamStatment , vppAdamIDs )
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "expand IN query for vpp titles" )
2024-07-24 17:39:23 +00:00
}
2025-04-10 22:29:15 +00:00
vppAdamStatement , vppAdamArgsNamedArgs , err := sqlx . Named ( vppAdamStatement , namedArgs )
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "build named query for vpp titles" )
}
vppAdamStatement = strings . ReplaceAll ( vppAdamStatement , "AND true" , matchClause )
args = append ( args , vppAdamArgsNamedArgs ... )
args = append ( args , vppAdamArgs ... )
if len ( matchArgs ) > 0 {
args = append ( args , matchArgs ... )
}
stmt += vppAdamStatement
2024-07-16 20:52:04 +00:00
}
2025-10-28 12:33:58 +00:00
if ! opts . VulnerableOnly && len ( inHouseIDs ) > 0 {
var inHouseStmt string
if len ( softwareTitleIDs ) > 0 || len ( vppAdamIDs ) > 0 {
inHouseStmt = ` UNION `
}
inHouseStmt += `
-- SELECT for in - house apps
% s
FROM
software_titles
INNER JOIN in_house_apps ON
software_titles . id = in_house_apps . title_id AND in_house_apps . platform = : host_platform AND in_house_apps . global_or_team_id = : global_or_team_id
WHERE
in_house_apps . id IN ( ? )
AND true
2025-11-07 22:30:51 +00:00
` + inHouseOnlySelfServiceClause + `
2025-10-28 12:33:58 +00:00
-- GROUP BY for in - house apps
% s
`
inHouseStmt , inHouseArgs , err := sqlx . In ( inHouseStmt , inHouseIDs )
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "expand IN query for in-house titles" )
}
inHouseStmt , inHouseArgsNamedArgs , err := sqlx . Named ( inHouseStmt , namedArgs )
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "build named query for in-house titles" )
}
inHouseStmt = strings . ReplaceAll ( inHouseStmt , "AND true" , matchClause )
args = append ( args , inHouseArgsNamedArgs ... )
args = append ( args , inHouseArgs ... )
if len ( matchArgs ) > 0 {
args = append ( args , matchArgs ... )
}
stmt += inHouseStmt
}
2025-04-10 22:29:15 +00:00
var countStmt string
2025-10-28 12:33:58 +00:00
var sprintfArgs [ ] any
// we do not scan vulnerabilities on vpp/in-house software available for install
includeSoftwareTitles := len ( softwareTitleIDs ) > 0
2025-04-10 22:29:15 +00:00
includeVPP := ! opts . VulnerableOnly && len ( vppAdamIDs ) > 0
2025-10-28 12:33:58 +00:00
includeInHouse := ! opts . VulnerableOnly && len ( inHouseIDs ) > 0
if includeSoftwareTitles {
sprintfArgs = append ( sprintfArgs , ` SELECT software_titles.id ` , softwareVulnerableJoin , ` GROUP BY software_titles.id ` )
}
if includeVPP {
sprintfArgs = append ( sprintfArgs , ` SELECT software_titles.id ` , ` GROUP BY software_titles.id ` )
}
if includeInHouse {
sprintfArgs = append ( sprintfArgs , ` SELECT software_titles.id ` , ` GROUP BY software_titles.id ` )
}
if len ( sprintfArgs ) == 0 {
2025-04-10 22:29:15 +00:00
return [ ] * fleet . HostSoftwareWithInstaller { } , & fleet . PaginationMetadata { } , nil
}
2025-10-28 12:33:58 +00:00
countStmt = fmt . Sprintf ( stmt , sprintfArgs ... )
2024-05-01 18:37:52 +00:00
2025-04-10 22:29:15 +00:00
if err := sqlx . GetContext (
ctx ,
ds . reader ( ctx ) ,
& titleCount ,
fmt . Sprintf ( "SELECT COUNT(id) FROM (%s) AS combined_results" , countStmt ) ,
args ... ,
) ; err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "get host software count" )
}
var replacements [ ] any
2025-10-28 12:33:58 +00:00
if len ( softwareTitleIDs ) > 0 {
2025-04-10 22:29:15 +00:00
replacements = append ( replacements ,
// For software installers
`
SELECT
software_titles . id ,
software_titles . name ,
software_titles . source AS source ,
2025-10-07 21:05:22 +00:00
software_titles . extension_for AS extension_for ,
2025-11-07 23:33:31 +00:00
software_titles . upgrade_code AS upgrade_code , -- should be empty or non - empty string for "programs" sourced software , null otherwise
2025-04-10 22:29:15 +00:00
software_installers . id AS installer_id ,
software_installers . self_service AS package_self_service ,
software_installers . filename AS package_name ,
software_installers . version AS package_version ,
software_installers . platform as package_platform ,
GROUP_CONCAT ( software . id ) AS software_id_list ,
GROUP_CONCAT ( software . source ) AS software_source_list ,
2025-10-07 21:05:22 +00:00
GROUP_CONCAT ( software . extension_for ) AS software_extension_for_list ,
2025-11-07 23:33:31 +00:00
GROUP_CONCAT ( software . upgrade_code ) AS software_upgrade_code_list ,
2025-04-10 22:29:15 +00:00
GROUP_CONCAT ( software . version ) AS version_list ,
GROUP_CONCAT ( software . bundle_identifier ) AS bundle_identifier_list ,
NULL AS vpp_app_adam_id_list ,
NULL AS vpp_app_version_list ,
NULL AS vpp_app_platform_list ,
NULL AS vpp_app_icon_url_list ,
2025-10-28 12:33:58 +00:00
NULL AS vpp_app_self_service_list ,
NULL AS in_house_app_id_list ,
NULL AS in_house_app_name_list ,
NULL AS in_house_app_version_list ,
2025-11-07 22:30:51 +00:00
NULL as in_house_app_platform_list ,
NULL as in_house_app_self_service_list
2025-04-10 22:29:15 +00:00
` , softwareVulnerableJoin, `
GROUP BY
software_titles . id ,
software_titles . name ,
software_titles . source ,
2025-10-07 21:05:22 +00:00
software_titles . extension_for ,
2025-11-07 23:33:31 +00:00
software_titles . upgrade_code ,
2025-04-10 22:29:15 +00:00
software_installers . id ,
software_installers . self_service ,
software_installers . filename ,
software_installers . version ,
software_installers . platform
` )
}
if includeVPP {
replacements = append ( replacements ,
// For vpp apps
`
SELECT
software_titles . id ,
software_titles . name ,
software_titles . source AS source ,
2025-10-07 21:05:22 +00:00
software_titles . extension_for AS extension_for ,
2025-11-07 23:33:31 +00:00
software_titles . upgrade_code AS upgrade_code , -- should always be null for vpp ( mac ) apps
2025-04-10 22:29:15 +00:00
NULL AS installer_id ,
NULL AS package_self_service ,
NULL AS package_name ,
NULL AS package_version ,
NULL as package_platform ,
NULL AS software_id_list ,
NULL AS software_source_list ,
2025-10-07 21:05:22 +00:00
NULL AS software_extension_for_list ,
2025-11-07 23:33:31 +00:00
NULL AS software_upgrade_code_list ,
2025-04-10 22:29:15 +00:00
NULL AS version_list ,
NULL AS bundle_identifier_list ,
GROUP_CONCAT ( vpp_apps . adam_id ) AS vpp_app_adam_id_list ,
GROUP_CONCAT ( vpp_apps . latest_version ) AS vpp_app_version_list ,
GROUP_CONCAT ( vpp_apps . platform ) as vpp_app_platform_list ,
GROUP_CONCAT ( vpp_apps . icon_url ) AS vpp_app_icon_url_list ,
2025-10-28 12:33:58 +00:00
GROUP_CONCAT ( vpp_apps_teams . self_service ) AS vpp_app_self_service_list ,
NULL AS in_house_app_id_list ,
NULL AS in_house_app_name_list ,
NULL AS in_house_app_version_list ,
2025-11-07 22:30:51 +00:00
NULL as in_house_app_platform_list ,
NULL as in_house_app_self_service_list
2025-10-28 12:33:58 +00:00
` , `
GROUP BY
software_titles . id ,
software_titles . name ,
software_titles . source ,
2025-11-07 23:33:31 +00:00
software_titles . extension_for ,
software_titles . upgrade_code
2025-10-28 12:33:58 +00:00
` )
}
if includeInHouse {
replacements = append ( replacements ,
// For in-house apps
`
SELECT
software_titles . id ,
software_titles . name ,
software_titles . source AS source ,
software_titles . extension_for AS extension_for ,
2025-11-07 23:33:31 +00:00
software_titles . upgrade_code AS upgrade_code ,
2025-10-28 12:33:58 +00:00
NULL AS installer_id ,
NULL AS package_self_service ,
NULL AS package_name ,
NULL AS package_version ,
NULL as package_platform ,
NULL AS software_id_list ,
NULL AS software_source_list ,
NULL AS software_extension_for_list ,
2025-11-07 23:33:31 +00:00
NULL AS software_upgrade_code_list ,
2025-10-28 12:33:58 +00:00
NULL AS version_list ,
NULL AS bundle_identifier_list ,
NULL AS vpp_app_adam_id_list ,
NULL AS vpp_app_version_list ,
NULL as vpp_app_platform_list ,
NULL AS vpp_app_icon_url_list ,
NULL AS vpp_app_self_service_list ,
GROUP_CONCAT ( in_house_apps . id ) AS in_house_app_id_list ,
2025-10-28 17:19:13 +00:00
GROUP_CONCAT ( in_house_apps . filename ) AS in_house_app_name_list ,
2025-10-28 12:33:58 +00:00
GROUP_CONCAT ( in_house_apps . version ) AS in_house_app_version_list ,
2025-11-07 22:30:51 +00:00
GROUP_CONCAT ( in_house_apps . platform ) as in_house_app_platform_list ,
GROUP_CONCAT ( in_house_apps . self_service ) as in_house_app_self_service_list
2025-04-10 22:29:15 +00:00
` , `
GROUP BY
software_titles . id ,
software_titles . name ,
2025-10-07 21:05:22 +00:00
software_titles . source ,
2025-11-07 23:33:31 +00:00
software_titles . extension_for ,
software_titles . upgrade_code
2025-04-10 22:29:15 +00:00
` )
}
stmt = fmt . Sprintf ( stmt , replacements ... )
stmt = fmt . Sprintf ( "SELECT * FROM (%s) AS combined_results" , stmt )
stmt , _ = appendListOptionsToSQL ( stmt , & opts . ListOptions )
if err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & hostSoftwareList , stmt , args ... ) ; err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "list host software" )
}
// collect install paths by software.id
installedPaths , err := ds . getHostSoftwareInstalledPaths ( ctx , host . ID )
2024-05-01 18:37:52 +00:00
if err != nil {
2025-04-10 22:29:15 +00:00
return nil , nil , ctxerr . Wrap ( ctx , err , "Could not get software installed paths" )
2024-05-01 18:37:52 +00:00
}
2025-04-10 22:29:15 +00:00
installedPathBySoftwareId := make ( map [ uint ] [ ] string )
pathSignatureInformation := make ( map [ uint ] [ ] fleet . PathSignatureInformation )
for _ , ip := range installedPaths {
installedPathBySoftwareId [ ip . SoftwareID ] = append ( installedPathBySoftwareId [ ip . SoftwareID ] , ip . InstalledPath )
pathSignatureInformation [ ip . SoftwareID ] = append ( pathSignatureInformation [ ip . SoftwareID ] , fleet . PathSignatureInformation {
InstalledPath : ip . InstalledPath ,
TeamIdentifier : ip . TeamIdentifier ,
2025-05-21 04:38:59 +00:00
HashSha256 : ip . ExecutableSHA256 ,
2025-04-10 22:29:15 +00:00
} )
2024-05-01 18:37:52 +00:00
}
2025-04-10 22:29:15 +00:00
// extract into vulnerabilitiesBySoftwareID
type softwareCVE struct {
SoftwareID uint ` db:"software_id" `
CVE string ` db:"cve" `
2024-05-01 18:37:52 +00:00
}
2025-04-10 22:29:15 +00:00
var softwareCVEs [ ] softwareCVE
2024-05-01 18:37:52 +00:00
if len ( softwareIDs ) > 0 {
2025-04-10 22:29:15 +00:00
cveStmt := `
SELECT
software_id ,
cve
FROM
software_cve
WHERE
software_id IN ( ? )
ORDER BY
software_id , cve
`
cveStmt , args , err = sqlx . In ( cveStmt , softwareIDs )
2024-05-01 18:37:52 +00:00
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "building query args to list cves" )
}
2025-04-10 22:29:15 +00:00
if err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & softwareCVEs , cveStmt , args ... ) ; err != nil {
2024-05-01 18:37:52 +00:00
return nil , nil , ctxerr . Wrap ( ctx , err , "list software cves" )
}
2025-04-10 22:29:15 +00:00
}
// group by softwareID
vulnerabilitiesBySoftwareID := make ( map [ uint ] [ ] string )
for _ , cve := range softwareCVEs {
vulnerabilitiesBySoftwareID [ cve . SoftwareID ] = append ( vulnerabilitiesBySoftwareID [ cve . SoftwareID ] , cve . CVE )
}
2025-08-08 20:49:32 +00:00
// Grab the automatic install policies, if any exist.
teamID := uint ( 0 ) // "No team" host
if host . TeamID != nil {
teamID = * host . TeamID // Team host
}
2025-10-28 12:33:58 +00:00
// NOTE: in-house apps do not support automatic install policies at the moment
policies , err := ds . getPoliciesBySoftwareTitleIDs ( ctx , append ( vppTitleIDs , softwareTitleIDs ... ) , teamID )
2025-08-01 15:22:14 +00:00
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "batch getting policies by software title IDs" )
}
policiesBySoftwareTitleId := make ( map [ uint ] [ ] fleet . AutomaticInstallPolicy , len ( policies ) )
for _ , p := range policies {
policiesBySoftwareTitleId [ p . TitleID ] = append ( policiesBySoftwareTitleId [ p . TitleID ] , p )
}
2025-10-28 12:33:58 +00:00
iconsBySoftwareTitleID , err := ds . GetSoftwareIconsByTeamAndTitleIds ( ctx , teamID , append ( append ( vppTitleIDs , inHouseTitleIDs ... ) , softwareTitleIDs ... ) )
2025-09-05 22:31:03 +00:00
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "get software icons by team and title IDs" )
}
2025-11-05 17:03:30 +00:00
displayNames , err := ds . getDisplayNamesByTeamAndTitleIds ( ctx , teamID , softwareTitleIDs )
if err != nil {
return nil , nil , ctxerr . Wrap ( ctx , err , "get software display names by team and title IDs" )
}
2025-04-10 22:29:15 +00:00
indexOfSoftwareTitle := make ( map [ uint ] uint )
deduplicatedList := make ( [ ] * hostSoftware , 0 , len ( hostSoftwareList ) )
for _ , softwareTitleRecord := range hostSoftwareList {
softwareTitle := bySoftwareTitleID [ softwareTitleRecord . ID ]
2025-05-09 19:52:11 +00:00
inventoriedVPPApp := hostVPPInstalledTitles [ softwareTitleRecord . ID ]
2025-10-28 12:33:58 +00:00
inventoriedInHouseApp := hostInHouseInstalledTitles [ softwareTitleRecord . ID ]
2025-04-10 22:29:15 +00:00
2025-04-22 03:53:06 +00:00
if softwareTitle != nil && softwareTitle . SoftwareID != nil {
// if we have a software id, that means that this record has been installed on the host,
// we should double check the hostInstalledSoftwareSet,
// but we want to make sure that software id is present on the InstalledVersions list to be processed
if s , ok := hostInstalledSoftwareSet [ * softwareTitle . SoftwareID ] ; ok {
softwareIDStr := strconv . FormatUint ( uint64 ( * softwareTitle . SoftwareID ) , 10 )
2025-05-09 19:52:11 +00:00
pushVersion ( softwareIDStr , softwareTitleRecord , * s )
}
}
if inventoriedVPPApp != nil && inventoriedVPPApp . SoftwareID != nil {
// Vpp app installed on the host, we need to push this into the installed versions list as well
if s , ok := hostInstalledSoftwareSet [ * inventoriedVPPApp . SoftwareID ] ; ok {
softwareIDStr := strconv . FormatUint ( uint64 ( * inventoriedVPPApp . SoftwareID ) , 10 )
pushVersion ( softwareIDStr , softwareTitleRecord , * s )
2025-04-22 03:53:06 +00:00
}
}
2025-10-28 12:33:58 +00:00
if inventoriedInHouseApp != nil && inventoriedInHouseApp . SoftwareID != nil {
// in-house app installed on the host, we need to push this into the installed versions list as well
if s , ok := hostInstalledSoftwareSet [ * inventoriedInHouseApp . SoftwareID ] ; ok {
softwareIDStr := strconv . FormatUint ( uint64 ( * inventoriedInHouseApp . SoftwareID ) , 10 )
pushVersion ( softwareIDStr , softwareTitleRecord , * s )
}
}
2025-04-22 03:53:06 +00:00
2025-04-10 22:29:15 +00:00
if softwareTitleRecord . SoftwareIDList != nil {
softwareIDList := strings . Split ( * softwareTitleRecord . SoftwareIDList , "," )
softwareSourceList := strings . Split ( * softwareTitleRecord . SoftwareSourceList , "," )
softwareVersionList := strings . Split ( * softwareTitleRecord . VersionList , "," )
softwareBundleIdentifierList := strings . Split ( * softwareTitleRecord . BundleIdentifierList , "," )
2024-05-01 18:37:52 +00:00
2025-04-10 22:29:15 +00:00
for index , softwareIdStr := range softwareIDList {
version := & fleet . HostSoftwareInstalledVersion { }
if softwareId , err := strconv . ParseUint ( softwareIdStr , 10 , 32 ) ; err == nil {
softwareId := uint ( softwareId )
if software , ok := bySoftwareID [ softwareId ] ; ok {
version . Version = softwareVersionList [ index ]
version . BundleIdentifier = softwareBundleIdentifierList [ index ]
version . Source = softwareSourceList [ index ]
version . LastOpenedAt = software . LastOpenedAt
version . SoftwareID = softwareId
version . SoftwareTitleID = softwareTitleRecord . ID
version . InstalledPaths = installedPathBySoftwareId [ softwareId ]
version . Vulnerabilities = vulnerabilitiesBySoftwareID [ softwareId ]
if version . Source == "apps" {
version . SignatureInformation = pathSignatureInformation [ softwareId ]
}
if storedIndex , ok := indexOfSoftwareTitle [ softwareTitleRecord . ID ] ; ok {
deduplicatedList [ storedIndex ] . InstalledVersions = append ( deduplicatedList [ storedIndex ] . InstalledVersions , version )
} else {
softwareTitleRecord . InstalledVersions = append ( softwareTitleRecord . InstalledVersions , version )
}
}
}
}
2024-05-01 18:37:52 +00:00
}
2025-04-10 22:29:15 +00:00
if softwareTitleRecord . VPPAppAdamIDList != nil {
vppAppAdamIDList := strings . Split ( * softwareTitleRecord . VPPAppAdamIDList , "," )
vppAppSelfServiceList := strings . Split ( * softwareTitleRecord . VPPAppSelfServiceList , "," )
vppAppVersionList := strings . Split ( * softwareTitleRecord . VPPAppVersionList , "," )
vppAppPlatformList := strings . Split ( * softwareTitleRecord . VPPAppPlatformList , "," )
vppAppIconURLList := strings . Split ( * softwareTitleRecord . VPPAppIconUrlList , "," )
if storedIndex , ok := indexOfSoftwareTitle [ softwareTitleRecord . ID ] ; ok {
softwareTitleRecord = deduplicatedList [ storedIndex ]
}
for index , vppAppAdamIdStr := range vppAppAdamIDList {
if vppAppAdamIdStr != "" {
softwareTitle = byVPPAdamID [ vppAppAdamIdStr ]
2025-04-18 14:18:05 +00:00
softwareTitleRecord . VPPAppAdamID = & vppAppAdamIdStr
2025-04-10 22:29:15 +00:00
}
vppAppSelfService := vppAppSelfServiceList [ index ]
if vppAppSelfService != "" {
if vppAppSelfService == "1" {
softwareTitleRecord . VPPAppSelfService = ptr . Bool ( true )
} else {
softwareTitleRecord . VPPAppSelfService = ptr . Bool ( false )
}
}
vppAppVersion := vppAppVersionList [ index ]
if vppAppVersion != "" {
softwareTitleRecord . VPPAppVersion = & vppAppVersion
}
vppAppPlatform := vppAppPlatformList [ index ]
if vppAppPlatform != "" {
softwareTitleRecord . VPPAppPlatform = & vppAppPlatform
}
VPPAppIconURL := vppAppIconURLList [ index ]
if VPPAppIconURL != "" {
softwareTitleRecord . VPPAppIconURL = & VPPAppIconURL
}
}
2024-05-01 18:37:52 +00:00
}
2025-04-10 22:29:15 +00:00
2025-10-28 12:33:58 +00:00
if softwareTitleRecord . InHouseAppIDList != nil {
inHouseAppIDList := strings . Split ( * softwareTitleRecord . InHouseAppIDList , "," )
inHouseAppVersionList := strings . Split ( * softwareTitleRecord . InHouseAppVersionList , "," )
inHouseAppPlatformList := strings . Split ( * softwareTitleRecord . InHouseAppPlatformList , "," )
inHouseAppNameList := strings . Split ( * softwareTitleRecord . InHouseAppNameList , "," )
2025-11-07 22:30:51 +00:00
inHouseAppSelfServiceList := strings . Split ( * softwareTitleRecord . InHouseAppSelfServiceList , "," )
2025-10-28 12:33:58 +00:00
if storedIndex , ok := indexOfSoftwareTitle [ softwareTitleRecord . ID ] ; ok {
softwareTitleRecord = deduplicatedList [ storedIndex ]
}
for index , inHouseAppIDStr := range inHouseAppIDList {
inHouseID64 , err := strconv . ParseUint ( inHouseAppIDStr , 10 , 32 )
if err != nil {
continue
}
inHouseID := uint ( inHouseID64 )
softwareTitle = byInHouseID [ inHouseID ]
softwareTitleRecord . InHouseAppID = & inHouseID
inHouseAppVersion := inHouseAppVersionList [ index ]
if inHouseAppVersion != "" {
softwareTitleRecord . InHouseAppVersion = & inHouseAppVersion
}
inHouseAppPlatform := inHouseAppPlatformList [ index ]
if inHouseAppPlatform != "" {
softwareTitleRecord . InHouseAppPlatform = & inHouseAppPlatform
}
inHouseAppName := inHouseAppNameList [ index ]
if inHouseAppName != "" {
softwareTitleRecord . InHouseAppName = & inHouseAppName
}
2025-11-07 22:30:51 +00:00
inHouseAppSelfService := inHouseAppSelfServiceList [ index ]
if inHouseAppSelfService != "" {
if inHouseAppSelfService == "1" {
softwareTitleRecord . InHouseAppSelfService = ptr . Bool ( true )
} else {
softwareTitleRecord . InHouseAppSelfService = ptr . Bool ( false )
}
}
2025-10-28 12:33:58 +00:00
}
}
2025-04-10 22:29:15 +00:00
if storedIndex , ok := indexOfSoftwareTitle [ softwareTitleRecord . ID ] ; ok {
softwareTitleRecord = deduplicatedList [ storedIndex ]
2024-05-01 18:37:52 +00:00
}
2025-04-10 22:29:15 +00:00
// Merge the data of `software title` into `softwareTitleRecord`
// We should try to move as much of these attributes into the `stmt` query
2025-04-18 14:18:05 +00:00
if softwareTitle != nil {
softwareTitleRecord . Status = softwareTitle . Status
softwareTitleRecord . LastInstallInstallUUID = softwareTitle . LastInstallInstallUUID
softwareTitleRecord . LastInstallInstalledAt = softwareTitle . LastInstallInstalledAt
softwareTitleRecord . LastUninstallScriptExecutionID = softwareTitle . LastUninstallScriptExecutionID
softwareTitleRecord . LastUninstallUninstalledAt = softwareTitle . LastUninstallUninstalledAt
if softwareTitle . PackageSelfService != nil {
softwareTitleRecord . PackageSelfService = softwareTitle . PackageSelfService
}
2024-05-01 18:37:52 +00:00
}
2025-04-10 22:29:15 +00:00
// promote the package name and version to the proper destination fields
if softwareTitleRecord . PackageName != nil {
2025-05-06 17:32:35 +00:00
if _ , ok := filteredBySoftwareTitleID [ softwareTitleRecord . ID ] ; ok {
hydrateHostSoftwareRecordFromDb ( softwareTitleRecord , softwareTitle )
}
2025-04-10 22:29:15 +00:00
}
2025-04-22 03:53:06 +00:00
// This happens when there is a software installed on the host but it is also a vpp record, so we want
// to grab the vpp data from the installed vpp record and merge it onto the software record
if installedVppRecord , ok := hostVPPInstalledTitles [ softwareTitleRecord . ID ] ; ok {
softwareTitleRecord . VPPAppAdamID = installedVppRecord . VPPAppAdamID
softwareTitleRecord . VPPAppVersion = installedVppRecord . VPPAppVersion
softwareTitleRecord . VPPAppPlatform = installedVppRecord . VPPAppPlatform
softwareTitleRecord . VPPAppIconURL = installedVppRecord . VPPAppIconURL
softwareTitleRecord . VPPAppSelfService = installedVppRecord . VPPAppSelfService
}
2025-04-10 22:29:15 +00:00
// promote the VPP app id and version to the proper destination fields
if softwareTitleRecord . VPPAppAdamID != nil {
2025-05-06 17:32:35 +00:00
if _ , ok := filteredByVPPAdamID [ * softwareTitleRecord . VPPAppAdamID ] ; ok {
promoteSoftwareTitleVPPApp ( softwareTitleRecord )
}
2025-04-10 22:29:15 +00:00
}
2025-10-28 12:33:58 +00:00
// This happens when there is a software installed on the host but it is
// also an in-house record, so we want to grab the in-house data from the
// installed record and merge it onto the software record
if installedInHouseRecord , ok := hostInHouseInstalledTitles [ softwareTitleRecord . ID ] ; ok {
softwareTitleRecord . InHouseAppID = installedInHouseRecord . InHouseAppID
softwareTitleRecord . InHouseAppName = installedInHouseRecord . InHouseAppName
softwareTitleRecord . InHouseAppVersion = installedInHouseRecord . InHouseAppVersion
softwareTitleRecord . InHouseAppPlatform = installedInHouseRecord . InHouseAppPlatform
2025-11-07 22:30:51 +00:00
softwareTitleRecord . InHouseAppSelfService = installedInHouseRecord . InHouseAppSelfService
2025-10-28 12:33:58 +00:00
}
// promote the in-house app id and version to the proper destination fields
if softwareTitleRecord . InHouseAppID != nil {
if _ , ok := filteredByInHouseID [ * softwareTitleRecord . InHouseAppID ] ; ok {
promoteSoftwareTitleInHouseApp ( softwareTitleRecord )
}
}
// NOTE: in-house apps do not support automatic install policies at the moment
2025-08-01 15:22:14 +00:00
if policies , ok := policiesBySoftwareTitleId [ softwareTitleRecord . ID ] ; ok {
2025-08-08 20:49:32 +00:00
switch {
case softwareTitleRecord . AppStoreApp != nil :
2025-08-01 15:22:14 +00:00
softwareTitleRecord . AppStoreApp . AutomaticInstallPolicies = policies
2025-08-08 20:49:32 +00:00
case softwareTitleRecord . SoftwarePackage != nil :
2025-08-01 15:22:14 +00:00
softwareTitleRecord . SoftwarePackage . AutomaticInstallPolicies = policies
2025-08-08 20:49:32 +00:00
default :
level . Warn ( ds . logger ) . Log (
"team_id" , teamID ,
"host_id" , host . ID ,
"software_title_id" , softwareTitleRecord . ID ,
"msg" , "software title record should have an associated VPP application or software package" ,
)
2025-08-01 15:22:14 +00:00
}
}
2025-09-05 22:31:03 +00:00
if icon , ok := iconsBySoftwareTitleID [ softwareTitleRecord . ID ] ; ok {
softwareTitleRecord . IconUrl = ptr . String ( icon . IconUrl ( ) )
}
2025-11-05 17:03:30 +00:00
if displayName , ok := displayNames [ softwareTitleRecord . ID ] ; ok {
softwareTitleRecord . DisplayName = displayName
}
2025-04-10 22:29:15 +00:00
if _ , ok := indexOfSoftwareTitle [ softwareTitleRecord . ID ] ; ! ok {
indexOfSoftwareTitle [ softwareTitleRecord . ID ] = uint ( len ( deduplicatedList ) )
deduplicatedList = append ( deduplicatedList , softwareTitleRecord )
2024-05-01 18:37:52 +00:00
}
}
2025-04-10 22:29:15 +00:00
hostSoftwareList = deduplicatedList
2024-05-01 18:37:52 +00:00
}
2024-05-27 14:53:41 +00:00
perPage := opts . ListOptions . PerPage
2024-05-01 18:37:52 +00:00
var metaData * fleet . PaginationMetadata
2024-05-27 14:53:41 +00:00
if opts . ListOptions . IncludeMetadata {
2024-05-01 18:37:52 +00:00
if perPage <= 0 {
perPage = defaultSelectLimit
}
2024-05-15 12:55:27 +00:00
metaData = & fleet . PaginationMetadata {
2024-05-27 14:53:41 +00:00
HasPreviousResults : opts . ListOptions . Page > 0 ,
2024-05-15 12:55:27 +00:00
TotalResults : titleCount ,
}
2024-10-18 17:38:26 +00:00
if len ( hostSoftwareList ) > int ( perPage ) { //nolint:gosec // dismiss G115
2024-05-01 18:37:52 +00:00
metaData . HasNextResults = true
hostSoftwareList = hostSoftwareList [ : len ( hostSoftwareList ) - 1 ]
}
}
software := make ( [ ] * fleet . HostSoftwareWithInstaller , 0 , len ( hostSoftwareList ) )
for _ , hs := range hostSoftwareList {
software = append ( software , & hs . HostSoftwareWithInstaller )
}
2025-07-01 15:19:42 +00:00
2024-05-01 18:37:52 +00:00
return software , metaData , nil
}
2024-05-03 16:03:59 +00:00
2025-04-09 20:08:51 +00:00
func ( ds * Datastore ) SetHostSoftwareInstallResult ( ctx context . Context , result * fleet . HostSoftwareInstallResultPayload ) ( wasCanceled bool , err error ) {
2024-05-03 16:03:59 +00:00
const stmt = `
UPDATE
host_software_installs
SET
pre_install_query_output = ? ,
install_script_exit_code = ? ,
install_script_output = ? ,
post_install_script_exit_code = ? ,
post_install_script_output = ?
WHERE
2024-05-07 15:28:16 +00:00
execution_id = ? AND
host_id = ?
2024-05-03 16:03:59 +00:00
`
2024-05-15 22:39:42 +00:00
truncateOutput := func ( output * string ) * string {
if output != nil {
output = ptr . String ( truncateScriptResult ( * output ) )
}
return output
}
2025-04-09 20:08:51 +00:00
err = ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2025-02-11 19:53:11 +00:00
res , err := tx . ExecContext ( ctx , stmt ,
truncateOutput ( result . PreInstallConditionOutput ) ,
result . InstallScriptExitCode ,
truncateOutput ( result . InstallScriptOutput ) ,
result . PostInstallScriptExitCode ,
truncateOutput ( result . PostInstallScriptOutput ) ,
result . InstallUUID ,
result . HostID ,
)
if err != nil {
return ctxerr . Wrap ( ctx , err , "update host software installation result" )
}
if n , _ := res . RowsAffected ( ) ; n == 0 {
return ctxerr . Wrap ( ctx , notFound ( "HostSoftwareInstall" ) . WithName ( result . InstallUUID ) , "host software installation not found" )
}
2025-03-03 17:44:09 +00:00
if result . Status ( ) != fleet . SoftwareInstallPending {
if _ , err := ds . activateNextUpcomingActivity ( ctx , tx , result . HostID , result . InstallUUID ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "activate next activity" )
}
2025-02-11 19:53:11 +00:00
}
2025-04-09 20:08:51 +00:00
// load whether or not the result was for a canceled activity
err = sqlx . GetContext ( ctx , tx , & wasCanceled , ` SELECT canceled FROM host_software_installs WHERE execution_id = ? ` , result . InstallUUID )
if err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
return err
}
2025-02-11 19:53:11 +00:00
return nil
} )
2025-04-09 20:08:51 +00:00
return wasCanceled , err
2024-05-03 16:03:59 +00:00
}
2024-08-26 22:30:56 +00:00
2025-11-05 13:47:07 +00:00
func ( ds * Datastore ) CreateIntermediateInstallFailureRecord ( ctx context . Context , result * fleet . HostSoftwareInstallResultPayload ) ( string , error ) {
2025-09-16 17:26:14 +00:00
// Get the original installation details first, including software title and package info
const getDetailsStmt = `
2025-10-08 14:24:38 +00:00
SELECT
2025-09-16 17:26:14 +00:00
hsi . software_installer_id ,
hsi . user_id ,
hsi . policy_id ,
hsi . self_service ,
hsi . created_at ,
2025-11-05 13:47:07 +00:00
si . title_id AS software_title_id ,
2025-09-16 17:26:14 +00:00
si . filename AS software_package ,
st . name AS software_title
FROM host_software_installs hsi
INNER JOIN software_installers si ON si . id = hsi . software_installer_id
INNER JOIN software_titles st ON st . id = si . title_id
WHERE hsi . execution_id = ? AND hsi . host_id = ?
`
var details struct {
SoftwareInstallerID uint ` db:"software_installer_id" `
UserID * uint ` db:"user_id" `
PolicyID * uint ` db:"policy_id" `
SelfService bool ` db:"self_service" `
CreatedAt time . Time ` db:"created_at" `
2025-11-05 13:47:07 +00:00
SoftwareTitleID * uint ` db:"software_title_id" `
2025-09-16 17:26:14 +00:00
SoftwarePackage string ` db:"software_package" `
SoftwareTitle string ` db:"software_title" `
}
if err := sqlx . GetContext ( ctx , ds . reader ( ctx ) , & details , getDetailsStmt , result . InstallUUID , result . HostID ) ; err != nil {
2025-11-05 13:47:07 +00:00
return "" , ctxerr . Wrap ( ctx , err , "get original install details" )
2025-09-16 17:26:14 +00:00
}
// Generate a deterministic execution ID for the failed attempt record
// Use UUID v5 with the original InstallUUID and RetriesRemaining to ensure idempotency
// Use a custom UUID namespace since our use case doesn't fit one of the standard UUID namespaces.
namespace := uuid . MustParse ( "a87db2d7-a372-4d2f-9bd2-afdcd9775ca8" )
failedExecID := uuid . NewSHA1 ( namespace , [ ] byte ( fmt . Sprintf ( "%s-%d" , result . InstallUUID , result . RetriesRemaining ) ) ) . String ( )
// Create or update a record with the failure details
// Use INSERT ... ON DUPLICATE KEY UPDATE to make this idempotent
const insertStmt = `
INSERT INTO host_software_installs (
execution_id ,
host_id ,
software_installer_id ,
user_id ,
policy_id ,
self_service ,
created_at ,
2025-11-05 13:47:07 +00:00
software_title_id ,
software_title_name ,
installer_filename ,
2025-09-16 17:26:14 +00:00
install_script_exit_code ,
install_script_output ,
pre_install_query_output ,
post_install_script_exit_code ,
post_install_script_output
2025-11-05 13:47:07 +00:00
) VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )
2025-09-16 17:26:14 +00:00
ON DUPLICATE KEY UPDATE
install_script_exit_code = VALUES ( install_script_exit_code ) ,
install_script_output = VALUES ( install_script_output ) ,
pre_install_query_output = VALUES ( pre_install_query_output ) ,
post_install_script_exit_code = VALUES ( post_install_script_exit_code ) ,
post_install_script_output = VALUES ( post_install_script_output ) ,
updated_at = CURRENT_TIMESTAMP ( 6 )
`
truncateOutput := func ( output * string ) * string {
if output != nil {
output = ptr . String ( truncateScriptResult ( * output ) )
}
return output
}
err := ds . withRetryTxx ( ctx , func ( tx sqlx . ExtContext ) error {
2025-11-05 13:47:07 +00:00
_ , err := tx . ExecContext ( ctx , insertStmt ,
2025-09-16 17:26:14 +00:00
failedExecID ,
result . HostID ,
details . SoftwareInstallerID ,
details . UserID ,
details . PolicyID ,
details . SelfService ,
details . CreatedAt ,
2025-11-05 13:47:07 +00:00
details . SoftwareTitleID ,
details . SoftwareTitle ,
details . SoftwarePackage ,
2025-09-16 17:26:14 +00:00
result . InstallScriptExitCode ,
truncateOutput ( result . InstallScriptOutput ) ,
truncateOutput ( result . PreInstallConditionOutput ) ,
result . PostInstallScriptExitCode ,
truncateOutput ( result . PostInstallScriptOutput ) ,
)
if err != nil {
return err
}
return nil
} )
if err != nil {
2025-11-05 13:47:07 +00:00
return "" , ctxerr . Wrap ( ctx , err , "create intermediate failure record" )
2025-09-16 17:26:14 +00:00
}
2025-11-05 13:47:07 +00:00
return failedExecID , nil
2025-09-16 17:26:14 +00:00
}
2024-08-26 22:30:56 +00:00
func getInstalledByFleetSoftwareTitles ( ctx context . Context , qc sqlx . QueryerContext , hostID uint ) ( [ ] fleet . SoftwareTitle , error ) {
// We are overloading vpp_apps_count to indicate whether installed title is a VPP app or not.
const stmt = `
SELECT
st . id ,
st . name ,
st . source ,
2025-10-07 21:05:22 +00:00
st . extension_for ,
2025-11-07 23:33:31 +00:00
st . upgrade_code ,
2024-08-26 22:30:56 +00:00
st . bundle_identifier ,
0 as vpp_apps_count
FROM software_titles st
INNER JOIN software_installers si ON si . title_id = st . id
2024-09-06 14:49:07 +00:00
INNER JOIN host_software_installs hsi ON hsi . host_id = : host_id AND hsi . software_installer_id = si . id
2025-04-08 15:23:28 +00:00
WHERE hsi . removed = 0 AND hsi . canceled = 0 AND hsi . status = : software_status_installed
2024-08-26 22:30:56 +00:00
UNION
SELECT
st . id ,
st . name ,
st . source ,
2025-10-07 21:05:22 +00:00
st . extension_for ,
2025-11-07 23:33:31 +00:00
st . upgrade_code ,
2024-08-26 22:30:56 +00:00
st . bundle_identifier ,
1 as vpp_apps_count
FROM software_titles st
INNER JOIN vpp_apps vap ON vap . title_id = st . id
2024-09-06 14:49:07 +00:00
INNER JOIN host_vpp_software_installs hvsi ON hvsi . host_id = : host_id AND hvsi . adam_id = vap . adam_id AND hvsi . platform = vap . platform
2024-08-26 22:30:56 +00:00
INNER JOIN nano_command_results ncr ON ncr . command_uuid = hvsi . command_uuid
2025-04-08 15:23:28 +00:00
WHERE hvsi . removed = 0 AND hvsi . canceled = 0 AND ncr . status = : mdm_status_acknowledged
2024-08-26 22:30:56 +00:00
`
2024-09-06 14:49:07 +00:00
selectStmt , args , err := sqlx . Named ( stmt , map [ string ] interface { } {
"host_id" : hostID ,
"software_status_installed" : fleet . SoftwareInstalled ,
"mdm_status_acknowledged" : fleet . MDMAppleStatusAcknowledged ,
} )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "build query to get installed software titles" )
}
2024-08-26 22:30:56 +00:00
var titles [ ] fleet . SoftwareTitle
2024-09-06 14:49:07 +00:00
if err := sqlx . SelectContext ( ctx , qc , & titles , selectStmt , args ... ) ; err != nil {
2024-08-26 22:30:56 +00:00
return nil , ctxerr . Wrap ( ctx , err , "get installed software titles" )
}
return titles , nil
}
func markHostSoftwareInstallsRemoved ( ctx context . Context , ex sqlx . ExtContext , hostID uint , titleIDs [ ] uint ) error {
const stmt = `
UPDATE host_software_installs hsi
INNER JOIN software_installers si ON hsi . software_installer_id = si . id
INNER JOIN software_titles st ON si . title_id = st . id
SET hsi . removed = 1
WHERE hsi . host_id = ? AND st . id IN ( ? )
`
stmtExpanded , args , err := sqlx . In ( stmt , hostID , titleIDs )
if err != nil {
return ctxerr . Wrap ( ctx , err , "build query args to mark host software install removed" )
}
if _ , err := ex . ExecContext ( ctx , stmtExpanded , args ... ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "mark host software install removed" )
}
return nil
}
func markHostVPPSoftwareInstallsRemoved ( ctx context . Context , ex sqlx . ExtContext , hostID uint , titleIDs [ ] uint ) error {
const stmt = `
UPDATE host_vpp_software_installs hvsi
INNER JOIN vpp_apps vap ON hvsi . adam_id = vap . adam_id AND hvsi . platform = vap . platform
INNER JOIN software_titles st ON vap . title_id = st . id
SET hvsi . removed = 1
WHERE hvsi . host_id = ? AND st . id IN ( ? )
`
stmtExpanded , args , err := sqlx . In ( stmt , hostID , titleIDs )
if err != nil {
return ctxerr . Wrap ( ctx , err , "build query args to mark host vpp software install removed" )
}
if _ , err := ex . ExecContext ( ctx , stmtExpanded , args ... ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "mark host vpp software install removed" )
}
return nil
}
2025-05-02 15:41:26 +00:00
func ( ds * Datastore ) NewSoftwareCategory ( ctx context . Context , name string ) ( * fleet . SoftwareCategory , error ) {
stmt := ` INSERT INTO software_categories (name) VALUES (?) `
res , err := ds . writer ( ctx ) . ExecContext ( ctx , stmt , name )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "new software category" )
}
r , _ := res . LastInsertId ( )
id := uint ( r ) //nolint:gosec // dismiss G115
return & fleet . SoftwareCategory { Name : name , ID : id } , nil
}
func ( ds * Datastore ) GetSoftwareCategoryIDs ( ctx context . Context , names [ ] string ) ( [ ] uint , error ) {
if len ( names ) == 0 {
return [ ] uint { } , nil
}
stmt := ` SELECT id FROM software_categories WHERE name IN (?) `
stmt , args , err := sqlx . In ( stmt , names )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "sqlx.In for get software category ids" )
}
var ids [ ] uint
if err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & ids , stmt , args ... ) ; err != nil {
if ! errors . Is ( err , sql . ErrNoRows ) {
return nil , ctxerr . Wrap ( ctx , err , "get software category ids" )
}
}
return ids , nil
}
func ( ds * Datastore ) GetCategoriesForSoftwareTitles ( ctx context . Context , softwareTitleIDs [ ] uint , teamID * uint ) ( map [ uint ] [ ] string , error ) {
if len ( softwareTitleIDs ) == 0 {
return map [ uint ] [ ] string { } , nil
}
stmt := `
SELECT
st . id AS title_id ,
sc . name AS software_category_name
FROM
software_installers si
JOIN software_titles st ON st . id = si . title_id
JOIN software_installer_software_categories sisc ON sisc . software_installer_id = si . id
JOIN software_categories sc ON sc . id = sisc . software_category_id
WHERE
st . id IN ( ? ) AND si . global_or_team_id = ?
UNION
SELECT
st . id AS title_id ,
sc . name AS software_category_name
FROM
vpp_apps va
JOIN vpp_apps_teams vat ON va . adam_id = vat . adam_id AND va . platform = vat . platform
JOIN software_titles st ON st . id = va . title_id
JOIN vpp_app_team_software_categories vatsc ON vatsc . vpp_app_team_id = vat . id
JOIN software_categories sc ON vatsc . software_category_id = sc . id
WHERE
2025-11-11 20:13:24 +00:00
st . id IN ( ? ) AND vat . global_or_team_id = ?
UNION
SELECT
st . id AS title_id ,
sc . name AS software_category_name
FROM
in_house_apps iha
JOIN software_titles st ON st . id = iha . title_id
JOIN in_house_app_software_categories ihasc ON ihasc . in_house_app_id = iha . id
JOIN software_categories sc ON ihasc . software_category_id = sc . id
WHERE
2025-11-12 14:06:16 +00:00
st . id IN ( ? ) AND iha . global_or_team_id = ?
2025-05-02 15:41:26 +00:00
`
var tmID uint
if teamID != nil {
tmID = * teamID
}
2025-11-11 20:13:24 +00:00
stmt , args , err := sqlx . In ( stmt , softwareTitleIDs , tmID , softwareTitleIDs , tmID , softwareTitleIDs , tmID )
2025-05-02 15:41:26 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "sqlx.In for get categories for software installers" )
}
var categories [ ] struct {
TitleID uint ` db:"title_id" `
CategoryName string ` db:"software_category_name" `
}
if err := sqlx . SelectContext ( ctx , ds . reader ( ctx ) , & categories , stmt , args ... ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err , "get categories for software installers" )
}
ret := make ( map [ uint ] [ ] string , len ( categories ) )
for _ , c := range categories {
ret [ c . TitleID ] = append ( ret [ c . TitleID ] , c . CategoryName )
}
return ret , nil
}