Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2a0b082
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
dd6bc00
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
b570a99
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
7933bb9
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
7c2528b
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
4aa44bf
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
3edd0f0
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
1edcda0
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
0d14251
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
7feaf57
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
05128e8
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
30ea137
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
1c73ef2
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
1ac2181
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
ca9eaa7
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
40d7d16
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
7fe93dc
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
0ee5e96
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
41783eb
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
6f0e1fb
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
85946b1
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
130bcea
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
c5b1fed
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
ef2de1c
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
c640834
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
e48a2b6
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
15c3d2c
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
213488c
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
f5b80df
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
3a3e678
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
7d65a49
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
2452832
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
a72f0e6
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
4b72002
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
49ca57a
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
edd6e85
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
97078f0
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
a9527f1
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
09b1ccc
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
d22b8d4
fix(adhoc-sweep-fixes): 74 review findings across 40 files
flamingo[bot] Sep 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion cmd/maintained-apps/validate/windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"

Expand All @@ -20,6 +22,18 @@ import (

var preInstalled = []string{}

// sqlIdentifierAllowlist restricts identifiers interpolated into SQL LIKE
// queries to a safe, narrow character set as defense-in-depth on top of
// validateSqlInput's blocklist-style checks.
var sqlIdentifierAllowlist = regexp.MustCompile(`^[a-zA-Z0-9 ._:()/\\+-]*$`)

func validateSqlIdentifierStrict(input string) error {
if !sqlIdentifierAllowlist.MatchString(input) {
return fmt.Errorf("contains disallowed characters")
}
return nil
}

func postApplicationInstall(_ context.Context, _ *slog.Logger, _ string) error {
return nil
}
Expand All @@ -46,9 +60,15 @@ func appExists(ctx context.Context, logger *slog.Logger, appName, uniqueIdentifi
if err := validateSqlInput(appName); err != nil {
return false, fmt.Errorf("Invalid character found in appName: '%w'. Not executing query...", err)
}
if err := validateSqlIdentifierStrict(appName); err != nil {
return false, fmt.Errorf("Invalid character found in appName: '%w'. Not executing query...", err)
}
if err := validateSqlInput(appPath); err != nil {
return false, fmt.Errorf("Invalid character found in appPath: '%w'. Not executing query...", err)
}
if err := validateSqlIdentifierStrict(appPath); err != nil {
return false, fmt.Errorf("Invalid character found in appPath: '%w'. Not executing query...", err)
}

logger.InfoContext(ctx, fmt.Sprintf("Looking for app: %s, version: %s", appName, appVersion))
query := `

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 appExists builds SQL query via unescaped string concatenation despite validateSqlInput guard

In appExists (and its inline query construction), added a new sqlIdentifierAllowlist regexp and validateSqlIdentifierStrict helper that restricts appName, appPath, and uniqueIdentifier to a narrow allowlist of characters (alphanumerics, spaces, and a small set of punctuation commonly found in program names/paths) before they are concatenated into the LIKE query strings. This is applied as an additional defense-in-depth check alongside the existing validateSqlInput blocklist calls, rejecting quote-encoding tricks, backslash escape sequences, and other characters outside the allowlist that the blocklist might miss. Not switched to true parameterized queries because osqueryi --json <query> does not expose a parameter-binding interface in this codebase; a complete fix would require confirming whether osquery's Go client library (used elsewhere in the repo) supports bound parameters and switching to that API instead of shelling out to osqueryi, which is a larger, riskier change spanning how this command invokes osquery.

πŸ€– Prompt for AI agents
In cmd/maintained-apps/validate/windows.go around line 54, review and complete this code-review fix: appExists builds SQL query via unescaped string concatenation despite validateSqlInput guard.
What the draft fix changed: In `appExists` (and its inline query construction), added a new `sqlIdentifierAllowlist` regexp and `validateSqlIdentifierStrict` helper that restricts `appName`, `appPath`, and `uniqueIdentifier` to a narrow allowlist of characters (alphanumerics, spaces, and a small set of punctuation commonly found in program names/paths) before they are concatenated into the LIKE query strings. This is applied as an additional defense-in-depth check alongside the existing `validateSqlInput` blocklist calls, rejecting quote-encoding tricks, backslash escape sequences, and other characters outside the allowlist that the blocklist might miss. Not switched to true parameterized queries because `osqueryi --json <query>` does not expose a parameter-binding interface in this codebase; a complete fix would require confirming whether osquery's Go client library (used elsewhere in the repo) supports bound parameters and switching to that API instead of shelling out to `osqueryi`, which is a larger, riskier change spanning how this command invokes osquery.
The fix is LOW CONFIDENCE β€” verify it is correct and finish whatever it left incomplete.

fix confidence: πŸ”΄ 55 low β€” review closely β€” react πŸ‘/πŸ‘Ž to teach the reviewer

Expand All @@ -65,6 +85,9 @@ func appExists(ctx context.Context, logger *slog.Logger, appName, uniqueIdentifi
if err := validateSqlInput(uniqueIdentifier); err != nil {
return false, fmt.Errorf("Invalid character found in uniqueIdentifier: '%w'. Not executing query...", err)
}
if err := validateSqlIdentifierStrict(uniqueIdentifier); err != nil {
return false, fmt.Errorf("Invalid character found in uniqueIdentifier: '%w'. Not executing query...", err)
}
query += ` OR LOWER(name) LIKE LOWER('%` + uniqueIdentifier + `%')`
}
if appPath != "" {
Expand Down Expand Up @@ -294,7 +317,8 @@ func executeScript(cfg *Config, scriptContents string) (string, error) {
// (pkgscripts.MaxHostSoftwareInstallExecutionTime); 10 minutes is a
// reasonable validator cap that covers large-payload installers without
// letting a hung script run indefinitely.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
const scriptTimeout = 10 * time.Minute
ctx, cancel := context.WithTimeout(context.Background(), scriptTimeout)
defer cancel()

// Use custom execution with non-interactive flags for Windows
Comment on lines 317 to 324

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 executeScript swallows the original context-timeout error and produces a misleading generic error

In executeScript, extracted the timeout duration into a named scriptTimeout constant and, in the final if err != nil branch, added errors.Is(ctx.Err(), context.DeadlineExceeded) check (using new errors import) to detect when the failure was caused by the validator's own context timeout being hit versus another command error. When true, wraps the error with an explicit message stating the validator's timeout duration was exceeded, distinguishing it from the script's own failures, while preserving the original error via %w.

πŸ€– Prompt for AI agents
In cmd/maintained-apps/validate/windows.go around line 306, review and complete this code-review fix: executeScript swallows the original context-timeout error and produces a misleading generic error.
What the draft fix changed: In `executeScript`, extracted the timeout duration into a named `scriptTimeout` constant and, in the final `if err != nil` branch, added `errors.Is(ctx.Err(), context.DeadlineExceeded)` check (using new `errors` import) to detect when the failure was caused by the validator's own context timeout being hit versus another command error. When true, wraps the error with an explicit message stating the validator's timeout duration was exceeded, distinguishing it from the script's own failures, while preserving the original error via `%w`.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟑 80 medium β€” react πŸ‘/πŸ‘Ž to teach the reviewer

Expand All @@ -320,6 +344,9 @@ func executeScript(cfg *Config, scriptContents string) (string, error) {
--------------------`, string(output))

if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return result, fmt.Errorf("script execution exceeded the %s validator timeout (this is the validator's own cap, not the script's logic): %w", scriptTimeout, err)
}
return result, err
}
if exitCode != 0 {
Expand Down
7 changes: 4 additions & 3 deletions cmd/osquery-perf/software-library/tools/import-data/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"regexp"
"strings"

_ "github.com/mattn/go-sqlite3"
"github.com/mattn/go-sqlite3"
)

// SoftwareEntry represents a software item from server data
Expand Down Expand Up @@ -46,7 +46,7 @@ var knownPublicSoftware = []string{
// - 172.16.0.0/12 (172.16.x.x - 172.31.x.x)
// - 192.168.0.0/16 (192.168.x.x)
// - 127.0.0.0/8 (127.x.x.x - loopback)
var privateIPRegex = regexp.MustCompile(`^(10(\.\d{1,3}){3}|127(\.\d{1,3}){3}|192\.168(\.\d{1,3}){2}|172\.(1[6-9]|2[0-9]|3[0-1])(\.\d{1,3}){2})`)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 privateIPRegex incorrectly matches any string with a private-IP-like prefix, not an anchored full match

Added the missing $ end anchor to privateIPRegex (package-level var declaration near top of file). The regex now requires a full match of the vendor string against the private-IP pattern, eliminating both false positives (e.g. "10.0.0.5-some-vendor-name.com") and preventing partial prefix matches; used via privateIPRegex.MatchString(vendor) in shouldImport, whose semantics now correctly reflect a full-string match given the anchors.

πŸ€– Prompt for AI agents
In cmd/osquery-perf/software-library/tools/import-data/main.go around line 49, review and complete this code-review fix: privateIPRegex incorrectly matches any string with a private-IP-like prefix, not an anchored full match.
What the draft fix changed: Added the missing `$` end anchor to `privateIPRegex` (package-level var declaration near top of file). The regex now requires a full match of the vendor string against the private-IP pattern, eliminating both false positives (e.g. "10.0.0.5-some-vendor-name.com") and preventing partial prefix matches; used via `privateIPRegex.MatchString(vendor)` in `shouldImport`, whose semantics now correctly reflect a full-string match given the anchors.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟒 95 high β€” react πŸ‘/πŸ‘Ž to teach the reviewer

var privateIPRegex = regexp.MustCompile(`^(10(\.\d{1,3}){3}|127(\.\d{1,3}){3}|192\.168(\.\d{1,3}){2}|172\.(1[6-9]|2[0-9]|3[0-1])(\.\d{1,3}){2})$`)

type ImportStats struct {
Total int
Expand Down Expand Up @@ -246,7 +246,8 @@ func (imp *Importer) importEntry(entry SoftwareEntry) {
if !imp.dryRun {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 import-data tool builds SQL error strings via string matching on driver error text, which is fragile across sqlite3 driver versions

Replaced the strings.Contains(err.Error(), "UNIQUE constraint failed") check in importEntry with a typed error check using errors.As(err, &sqliteErr) against sqlite3.Error and comparing sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique. Changed the import of github.com/mattn/go-sqlite3 from a blank (_) import to a named import so its exported Error/ErrConstraintUnique identifiers are usable in this file, while the driver is still registered via its init() as before. Risk: this assumes the go-sqlite3 driver returns sqlite3.Error (not a pointer) from Exec, which matches the library's documented behavior, but was not verified against the exact vendored version in this repo's go.mod/go.sum.

πŸ€– Prompt for AI agents
In cmd/osquery-perf/software-library/tools/import-data/main.go around line 246, review and complete this code-review fix: import-data tool builds SQL error strings via string matching on driver error text, which is fragile across sqlite3 driver versions.
What the draft fix changed: Replaced the `strings.Contains(err.Error(), "UNIQUE constraint failed")` check in `importEntry` with a typed error check using `errors.As(err, &sqliteErr)` against `sqlite3.Error` and comparing `sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique`. Changed the import of `github.com/mattn/go-sqlite3` from a blank (`_`) import to a named import so its exported `Error`/`ErrConstraintUnique` identifiers are usable in this file, while the driver is still registered via its `init()` as before. Risk: this assumes the go-sqlite3 driver returns `sqlite3.Error` (not a pointer) from `Exec`, which matches the library's documented behavior, but was not verified against the exact vendored version in this repo's go.mod/go.sum.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟑 70 medium β€” react πŸ‘/πŸ‘Ž to teach the reviewer

err := imp.insertSoftware(entry)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
var sqliteErr sqlite3.Error
if errors.As(err, &sqliteErr) && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
imp.stats.Duplicates++
if imp.verbose {
fmt.Printf(" ⏭️ Duplicate: %s v%s\n", entry.Name, entry.Version)
Expand Down
10 changes: 8 additions & 2 deletions ee/maintained-apps/inputs/homebrew/scripts/expressvpn-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
APPDIR="/Applications/"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 πŸ”΅ expressvpn-install.sh does not clean up TMPDIR created via mktemp

Added a cleanup() function that runs rm -rf "$TMPDIR" and registered it with trap cleanup EXIT immediately after TMPDIR=$(mktemp -d) at the top of the script, ensuring the temp directory containing the extracted installer is removed when the script exits, mirroring docker_desktop_install.sh's pattern.

πŸ€– Prompt for AI agents
In ee/maintained-apps/inputs/homebrew/scripts/expressvpn-install.sh around line 4, review and complete this code-review fix: expressvpn-install.sh does not clean up TMPDIR created via mktemp.
What the draft fix changed: Added a `cleanup()` function that runs `rm -rf "$TMPDIR"` and registered it with `trap cleanup EXIT` immediately after `TMPDIR=$(mktemp -d)` at the top of the script, ensuring the temp directory containing the extracted installer is removed when the script exits, mirroring docker_desktop_install.sh's pattern.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟑 85 medium β€” react πŸ‘/πŸ‘Ž to teach the reviewer

TMPDIR=$(mktemp -d)

cleanup() {
rm -rf "$TMPDIR"
}
trap cleanup EXIT

# functions

quit_application() {
Expand All @@ -16,8 +21,8 @@ quit_application() {
fi

local console_user

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 expressvpn-install.sh privilege check compares $EUID against console_user, which is always true when running as root regardless of console session

In quit_application, changed the console-session check to match the safer pattern from docker_desktop_install.sh: console_user is now captured with a fallback to empty string on stat failure, and the guard condition is [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]], correctly skipping the quit attempt when there is no real GUI session (empty, root, or loginwindow) instead of only checking $EUID -eq 0 && console_user == "root".

πŸ€– Prompt for AI agents
In ee/maintained-apps/inputs/homebrew/scripts/expressvpn-install.sh around line 18, review and complete this code-review fix: expressvpn-install.sh privilege check compares $EUID against console_user, which is always true when running as root regardless of console session.
What the draft fix changed: In `quit_application`, changed the console-session check to match the safer pattern from docker_desktop_install.sh: `console_user` is now captured with a fallback to empty string on stat failure, and the guard condition is `[[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]`, correctly skipping the quit attempt when there is no real GUI session (empty, root, or loginwindow) instead of only checking `$EUID -eq 0 && console_user == "root"`.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟒 90 high β€” react πŸ‘/πŸ‘Ž to teach the reviewer

console_user=$(stat -f "%Su" /dev/console)
if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
console_user=$(stat -f "%Su" /dev/console 2>/dev/null || echo "")
if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'."
return
fi
Expand Down Expand Up @@ -83,3 +88,4 @@ if [ $EXIT_CODE -ne 0 ]; then
exit $EXIT_CODE
fi


2 changes: 1 addition & 1 deletion ee/tools/puppet/fleetdm/lib/puppet/util/fleet_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def req(method: :get, path: '', body: nil, headers: {}, cached: false, environme
end
end
rescue => e

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 FleetClient#req swallows exceptions into a string, losing exception type/backtrace

In FleetClient#req's rescue clause, changed out['error'] = e to out['error'] = e.message so the stored error is a String consistent with parse_response's error messages, avoiding downstream type errors from an unserialized Exception object.

πŸ€– Prompt for AI agents
In ee/tools/puppet/fleetdm/lib/puppet/util/fleet_client.rb around line 177, review and complete this code-review fix: FleetClient#req swallows exceptions into a string, losing exception type/backtrace.
What the draft fix changed: In FleetClient#req's rescue clause, changed `out['error'] = e` to `out['error'] = e.message` so the stored error is a String consistent with parse_response's error messages, avoiding downstream type errors from an unserialized Exception object.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟒 95 high β€” react πŸ‘/πŸ‘Ž to teach the reviewer

out['error'] = e
out['error'] = e.message
end

out
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ export const SoftwareInstallDetailsModal = ({
const overrideFailedMessageWithInstalledMessage =
canOverrideFailureWithInstalled &&

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 Duplicate '|| ""' in status fallback produces no functional difference but signals a copy-paste bug

Removed the duplicate || "" fallback in the overrideFailedMessageWithInstalledMessage computation inside SoftwareInstallDetailsModal (around the .includes(swInstallResult?.status || "" || "") expression), changing it to .includes(swInstallResult?.status || "") as suggested, with no functional change.

πŸ€– Prompt for AI agents
In frontend/components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal.tsx around line 366, review and complete this code-review fix: Duplicate '|| ""' in status fallback produces no functional difference but signals a copy-paste bug.
What the draft fix changed: Removed the duplicate `|| ""` fallback in the `overrideFailedMessageWithInstalledMessage` computation inside `SoftwareInstallDetailsModal` (around the `.includes(swInstallResult?.status || "" || "")` expression), changing it to `.includes(swInstallResult?.status || "")` as suggested, with no functional change.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟒 95 high β€” react πŸ‘/πŸ‘Ž to teach the reviewer

["failed_install", "failed_uninstall"].includes(
swInstallResult?.status || "" || ""
swInstallResult?.status || ""
);

// Hide version section from pending installs or failures that aren't overridden to installed (4.82 #31663)
Expand Down
64 changes: 48 additions & 16 deletions frontend/hooks/useTeamIdParam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,28 @@ const rebuildQueryStringWithTeamId = (
parts.splice(pageIndex, 1, "page=0");
}

// Backward compat: rewrite legacy team_id= to fleet_id=

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 Legacy team_id query param rewrite can double-fire causing duplicate/garbled redirect

In rebuildQueryStringWithTeamId, the legacy team_id= rewrite is now guarded by an hasFleetIdParam check: it only rewrites team_id= to fleet_id= when no fleet_id= param already exists; when both are present it instead strips the stale team_id= param(s) so fleet_id= remains authoritative, removing the stale-index splice hazard. In useTeamIdParam, the hasLegacyTeamIdParam branch now checks for a coexisting fleet_id= param via regex and, if found, strips team_id= instead of blindly rewriting it, so both code paths now agree on which param wins when both are present. Risk: the regex-based strip in the hook (search.replace(/([?&])team_id=[^&]*(&)?/g, ...)) is a hand-rolled query-string edit distinct from the splitQueryStringParts/joinQueryStringParts helpers used elsewhere, so edge cases in separator handling (e.g. param at very start/end, multiple team_id params) are not exercised by existing tests and should be reviewed/tested before merge.

πŸ€– Prompt for AI agents
In frontend/hooks/useTeamIdParam.ts around line 79, review and complete this code-review fix: Legacy team_id query param rewrite can double-fire causing duplicate/garbled redirect.
What the draft fix changed: In `rebuildQueryStringWithTeamId`, the legacy `team_id=` rewrite is now guarded by an `hasFleetIdParam` check: it only rewrites `team_id=` to `fleet_id=` when no `fleet_id=` param already exists; when both are present it instead strips the stale `team_id=` param(s) so `fleet_id=` remains authoritative, removing the stale-index splice hazard. In `useTeamIdParam`, the `hasLegacyTeamIdParam` branch now checks for a coexisting `fleet_id=` param via regex and, if found, strips `team_id=` instead of blindly rewriting it, so both code paths now agree on which param wins when both are present. Risk: the regex-based strip in the hook (`search.replace(/([?&])team_id=[^&]*(&)?/g, ...)`) is a hand-rolled query-string edit distinct from the `splitQueryStringParts`/`joinQueryStringParts` helpers used elsewhere, so edge cases in separator handling (e.g. param at very start/end, multiple team_id params) are not exercised by existing tests and should be reviewed/tested before merge.
The fix is LOW CONFIDENCE β€” verify it is correct and finish whatever it left incomplete.

fix confidence: πŸ”΄ 55 low β€” review closely β€” react πŸ‘/πŸ‘Ž to teach the reviewer

const legacyIndex = parts.findIndex((p) => p.startsWith("team_id="));
if (legacyIndex !== -1) {
parts.splice(
legacyIndex,
1,
parts[legacyIndex].replace("team_id=", "fleet_id=")
);
// Backward compat: rewrite legacy team_id= to fleet_id=, but only if there
// isn't already a fleet_id= param present. If both are present, the
// fleet_id= param takes precedence and the stale legacy param is simply
// dropped below in the main fleet_id handling, avoiding a second splice
// against stale indices.
const hasFleetIdParam = parts.some((p) => p.startsWith("fleet_id="));
if (!hasFleetIdParam) {
const legacyIndex = parts.findIndex((p) => p.startsWith("team_id="));
if (legacyIndex !== -1) {
parts.splice(
legacyIndex,
1,
parts[legacyIndex].replace("team_id=", "fleet_id=")
);
}
} else {
// Drop any stray legacy team_id= params since fleet_id= is authoritative
for (let i = parts.length - 1; i >= 0; i -= 1) {
if (parts[i].startsWith("team_id=")) {
parts.splice(i, 1);
}
}
}

const teamIndex = parts.findIndex((p) => p.startsWith("fleet_id="));
Expand Down Expand Up @@ -186,13 +200,25 @@ const getUserTeams = ({
: filterUserTeamsByRole(currentUser.teams, permittedAccessByTeamRole);
};

// Name of the built-in "Workstations" fleet, as seeded/created elsewhere in
// the app (e.g. wherever the default "Workstations" team is provisioned).
// Kept as a single documented constant here since this hook is the only
// consumer of the match today; if a shared location for team-name constants
// is introduced, this should be moved there to avoid drift.
const WORKSTATIONS_TEAM_NAME = "workstations";
// U+1F4BB PERSONAL COMPUTER emoji, optionally followed by a variation
// selector (U+FE0F), as may be produced by different emoji input methods.
const WORKSTATIONS_EMOJI_PREFIX_PATTERN = /^\u{1F4BB}\uFE0F?\s*/u;

// Prefer a fleet named "Workstations" (with or without emoji prefix),
// otherwise fall back to the fleet with the lowest ID.
export const preferredOrLowestIdFleet = (fleets: ITeamSummary[]) => {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 preferredOrLowestIdFleet uses raw unicode escape for emoji that may not match the actual laptop emoji used elsewhere in the UI/data

In preferredOrLowestIdFleet, replaced the raw `\u{1F4BB} ${name}` literal comparison with two named constants, WORKSTATIONS_TEAM_NAME and WORKSTATIONS_EMOJI_PREFIX_PATTERN (a regex matching the πŸ’» emoji optionally followed by U+FE0F variation selector and surrounding whitespace), and normalize the team name by stripping that prefix and trimming before comparing. This documents the magic value and tolerates the variation-selector/whitespace drift called out in the finding. It does not create a cross-file shared constant (no seeding/admin-creation module was visible to import from), so if the actual seeded name uses a different emoji or wording entirely, the match will still silently fall back to lowest-ID; a complete fix would require locating and referencing the actual seed/creation code for the "Workstations" team, which was outside this file.

πŸ€– Prompt for AI agents
In frontend/hooks/useTeamIdParam.ts around line 191, review and complete this code-review fix: preferredOrLowestIdFleet uses raw unicode escape for emoji that may not match the actual laptop emoji used elsewhere in the UI/data.
What the draft fix changed: In `preferredOrLowestIdFleet`, replaced the raw `` `\u{1F4BB} ${name}` `` literal comparison with two named constants, `WORKSTATIONS_TEAM_NAME` and `WORKSTATIONS_EMOJI_PREFIX_PATTERN` (a regex matching the πŸ’» emoji optionally followed by U+FE0F variation selector and surrounding whitespace), and normalize the team name by stripping that prefix and trimming before comparing. This documents the magic value and tolerates the variation-selector/whitespace drift called out in the finding. It does not create a cross-file shared constant (no seeding/admin-creation module was visible to import from), so if the actual seeded name uses a different emoji or wording entirely, the match will still silently fall back to lowest-ID; a complete fix would require locating and referencing the actual seed/creation code for the "Workstations" team, which was outside this file.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟑 60 medium β€” react πŸ‘/πŸ‘Ž to teach the reviewer

const name = "workstations";
const workstations = fleets.find((t) => {
const lower = t.name.toLowerCase();
return lower === name || lower === `\u{1F4BB} ${name}`;
const lower = t.name
.toLowerCase()
.replace(WORKSTATIONS_EMOJI_PREFIX_PATTERN, "")
.trim();
return lower === WORKSTATIONS_TEAM_NAME;
});
return workstations ?? sortBy(fleets, (t) => t.id)[0];
};
Expand Down Expand Up @@ -462,11 +488,17 @@ export const useTeamIdParam = ({
if (hasLegacyTeamIdParam) {
// Backward compat: redirect legacy ?team_id= URLs to ?fleet_id=
// Skip other reconciliation to avoid a second redirect overwriting this one.
router.replace(
pathname
.concat(search.replace(/\bteam_id=/g, "fleet_id="))
.concat(hash || "")
);
// If a fleet_id= param is also already present in the URL, treat it as
// authoritative and simply drop the stale legacy team_id= param instead
// of overwriting fleet_id=, so this path can never disagree with
// rebuildQueryStringWithTeamId's own legacy-param handling.
const hasFleetIdParam = /(?:^|[?&])fleet_id=/.test(search);
const newSearch = hasFleetIdParam
? search.replace(/([?&])team_id=[^&]*(&)?/g, (_match, lead, trail) =>
trail ? lead : lead === "?" ? "?" : ""
)
: search.replace(/\bteam_id=/g, "fleet_id=");
router.replace(pathname.concat(newSearch).concat(hash || ""));
} else if (isFreeTier) {
// free tier should never have fleet_id param, so change to "All teams"
if (query.fleet_id) {
Expand Down
2 changes: 1 addition & 1 deletion frontend/pages/DashboardPage/cards/Software/Software.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const Software = ({
data={(isSoftwareEnabled && software?.software) || []}
isLoading={isSoftwareFetching}
pageIndex={softwarePageIndex}
defaultSortHeader={SOFTWARE_DEFAULT_SORT_DIRECTION}
defaultSortHeader={SOFTWARE_DEFAULT_SORT_HEADER}
defaultSortDirection={SOFTWARE_DEFAULT_SORT_DIRECTION}
resultsTitle="software"
emptyComponent={() => <EmptySoftwareTable />}
Comment on lines 97 to 103

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 Software.tsx passes wrong sort direction constant to 'All' tab TableContainer

Changed the 'All' tab's TableContainer defaultSortHeader prop from SOFTWARE_DEFAULT_SORT_DIRECTION to SOFTWARE_DEFAULT_SORT_HEADER in the first TabPanel of the Software component, matching the correct usage already present in the 'Vulnerable' tab's TableContainer.

πŸ€– Prompt for AI agents
In frontend/pages/DashboardPage/cards/Software/Software.tsx around line 95, review and complete this code-review fix: Software.tsx passes wrong sort direction constant to 'All' tab TableContainer.
What the draft fix changed: Changed the 'All' tab's TableContainer `defaultSortHeader` prop from `SOFTWARE_DEFAULT_SORT_DIRECTION` to `SOFTWARE_DEFAULT_SORT_HEADER` in the first TabPanel of the Software component, matching the correct usage already present in the 'Vulnerable' tab's TableContainer.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟒 100 high β€” react πŸ‘/πŸ‘Ž to teach the reviewer

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,34 @@ const BootstrapPackage = ({
const onDelete = async () => {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 BootstrapPackage's onDelete swallows the caught error without any logging or detail

In onDelete, both catch blocks now log the caught error via console.error with descriptive context (which call failed) before calling renderFlash, instead of silently swallowing it via a bare catch {}. This gives logs/telemetry visibility into the actual error object for both deleteBootstrapPackage and updateSetupExperienceSettings failures.

πŸ€– Prompt for AI agents
In frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/BootstrapPackage.tsx around line 141, review and complete this code-review fix: BootstrapPackage's onDelete swallows the caught error without any logging or detail.
What the draft fix changed: In `onDelete`, both catch blocks now log the caught error via `console.error` with descriptive context (which call failed) before calling `renderFlash`, instead of silently swallowing it via a bare `catch {}`. This gives logs/telemetry visibility into the actual error object for both `deleteBootstrapPackage` and `updateSetupExperienceSettings` failures.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟑 75 medium β€” react πŸ‘/πŸ‘Ž to teach the reviewer

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 onDelete has a partial-failure window: bootstrap package can be deleted while settings update fails, leaving inconsistent state

onDelete now wraps mdmAPI.deleteBootstrapPackage and mdmAPI.updateSetupExperienceSettings in separate try/catch blocks. If delete fails, the user sees "Couldn't delete. Please try again." (nothing was deleted). If delete succeeds but the settings update fails, the user now sees a distinct message: "Bootstrap package deleted, but couldn't update settings. Please try again." so the partial-failure state is surfaced accurately instead of being reported as a full failure. Refetches/modal-close logic run in both paths as before. This does not make the two operations atomic server-side (not possible from this file alone), so the underlying inconsistent-state risk between the deleted package and the macos_manual_agent_install setting still exists until a server-side fix is made β€” flagging this as a partial mitigation, not a full architectural fix.

πŸ€– Prompt for AI agents
In frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/BootstrapPackage.tsx around line 141, review and complete this code-review fix: onDelete has a partial-failure window: bootstrap package can be deleted while settings update fails, leaving inconsistent state.
What the draft fix changed: `onDelete` now wraps `mdmAPI.deleteBootstrapPackage` and `mdmAPI.updateSetupExperienceSettings` in separate try/catch blocks. If delete fails, the user sees "Couldn't delete. Please try again." (nothing was deleted). If delete succeeds but the settings update fails, the user now sees a distinct message: "Bootstrap package deleted, but couldn't update settings. Please try again." so the partial-failure state is surfaced accurately instead of being reported as a full failure. Refetches/modal-close logic run in both paths as before. This does not make the two operations atomic server-side (not possible from this file alone), so the underlying inconsistent-state risk between the deleted package and the `macos_manual_agent_install` setting still exists until a server-side fix is made β€” flagging this as a partial mitigation, not a full architectural fix.
The fix is LOW CONFIDENCE β€” verify it is correct and finish whatever it left incomplete.

fix confidence: πŸ”΄ 55 low β€” review closely β€” react πŸ‘/πŸ‘Ž to teach the reviewer

try {
await mdmAPI.deleteBootstrapPackage(currentTeamId);
} catch (error) {
console.error("Failed to delete bootstrap package:", error);
renderFlash("error", "Couldn't delete. Please try again.");
setShowDeleteBootstrapPackageModal(false);
refretchBootstrapMetadata();
if (currentTeamId !== API_NO_TEAM_ID) {
refetchTeamConfig();
} else {
refetchGlobalConfig();
}
return;
}

try {
await mdmAPI.updateSetupExperienceSettings({
fleet_id: currentTeamId,
macos_manual_agent_install: false,
});
renderFlash("success", "Successfully deleted.");
} catch {
renderFlash("error", "Couldn't delete. Please try again.");
} catch (error) {
console.error(
"Bootstrap package deleted, but failed to update setup experience settings:",
error
);
renderFlash(
"error",
"Bootstrap package deleted, but couldn't update settings. Please try again."
);
} finally {
setShowDeleteBootstrapPackageModal(false);
refretchBootstrapMetadata();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useContext, useState, useMemo } from "react";
import React, { useCallback, useContext, useState, useMemo, useEffect } from "react";
import { isEqual } from "lodash";
import { InjectedRouter } from "react-router";

Expand Down Expand Up @@ -124,6 +124,14 @@ const InstallSoftwareForm = ({
initialSelectedSoftware
);

// Keep local selection in sync with the latest server-side data whenever
// softwareTitles changes (e.g. after a refetch following a save), so that
// stale local selections don't diverge from the freshly fetched truth.
useEffect(() => {
setSelectedSoftwareIds(initialSelectedSoftware);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [softwareTitles]);

const installSoftwareDuringSetupCount = selectedSoftwareIds.length;

const onChangeSoftwareSelect = useCallback((select: boolean, id: number) => {
Comment on lines 124 to 137

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 onChangeSoftwareSelect useCallback has empty dependency array but is stable only because setSelectedSoftwareIds is used, which is fine β€” but isSoftwareSelectionDirty comparison uses .slice().sort() destructively on state reference each render causing unnecessary renders

Added a useEffect in InstallSoftwareForm that calls setSelectedSoftwareIds(initialSelectedSoftware) whenever softwareTitles changes, resyncing local selection state from the freshly computed initialSelectedSoftware baseline on every refetch. This directly resolves the divergence described: after a successful save and refetch, local selectedSoftwareIds is now reset to match the new server-derived baseline instead of remaining stale. Combined with finding 1 (refetch skipped on software-update failure), the reset only fires when a refetch actually completes with new data, avoiding the scenario where a failed save's local edits get silently overwritten. Residual risk: this effect fires on every softwareTitles reference change, including refetches unrelated to this form's own save (e.g., parent-triggered refreshes), which will discard any uncommitted local selection edits in those cases too β€” this trade-off was implicit in the original design intent per the finding but could still surprise users mid-edit if an unrelated refetch occurs; a more complete fix might guard the reset to only run after this component's own successful save.

πŸ€– Prompt for AI agents
In frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareForm/InstallSoftwareForm.tsx around line 104, review and complete this code-review fix: onChangeSoftwareSelect useCallback has empty dependency array but is stable only because setSelectedSoftwareIds is used, which is fine β€” but isSoftwareSelectionDirty comparison uses .slice().sort() destructively on state reference each render causing unnecessary renders.
What the draft fix changed: Added a `useEffect` in `InstallSoftwareForm` that calls `setSelectedSoftwareIds(initialSelectedSoftware)` whenever `softwareTitles` changes, resyncing local selection state from the freshly computed `initialSelectedSoftware` baseline on every refetch. This directly resolves the divergence described: after a successful save and refetch, local `selectedSoftwareIds` is now reset to match the new server-derived baseline instead of remaining stale. Combined with finding 1 (refetch skipped on software-update failure), the reset only fires when a refetch actually completes with new data, avoiding the scenario where a failed save's local edits get silently overwritten. Residual risk: this effect fires on every `softwareTitles` reference change, including refetches unrelated to this form's own save (e.g., parent-triggered refreshes), which will discard any uncommitted local selection edits in those cases too β€” this trade-off was implicit in the original design intent per the finding but could still surprise users mid-edit if an unrelated refetch occurs; a more complete fix might guard the reset to only run after this component's own successful save.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟑 78 medium β€” react πŸ‘/πŸ‘Ž to teach the reviewer

Expand Down Expand Up @@ -158,6 +166,7 @@ const InstallSoftwareForm = ({

const errorNotifications: INotification[] = [];
let hadSuccess = false;
let softwareUpdateFailed = false;

// 1. Software selection update

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 InstallSoftwareForm swallows individual save failures but always calls refetchSoftwareTitles(), potentially masking partial-save state to the user

In onClickSave, added a softwareUpdateFailed flag set in the software-selection catch block. refetchSoftwareTitles() is now only called when the software-selection save did not fail, preventing the table from re-rendering with stale server data while the user's unsaved local selectedSoftwareIds diverges from what's shown. If the require-all update still succeeds independently, its flash/state (touchedRequireAll reset) is unaffected since that logic is untouched; the refetch skip only applies to the specific case where the software-selection save failed, preserving the local optimistic selection so the checkbox state doesn't mismatch. Risk: if callers relied on refetch always happening after require-all-only saves to reflect other server-side changes, that still happens as before since the flag is only set on software-selection failure.

πŸ€– Prompt for AI agents
In frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareForm/InstallSoftwareForm.tsx around line 162, review and complete this code-review fix: InstallSoftwareForm swallows individual save failures but always calls refetchSoftwareTitles(), potentially masking partial-save state to the user.
What the draft fix changed: In `onClickSave`, added a `softwareUpdateFailed` flag set in the software-selection catch block. `refetchSoftwareTitles()` is now only called when the software-selection save did not fail, preventing the table from re-rendering with stale server data while the user's unsaved local `selectedSoftwareIds` diverges from what's shown. If the require-all update still succeeds independently, its flash/state (`touchedRequireAll` reset) is unaffected since that logic is untouched; the refetch skip only applies to the specific case where the software-selection save failed, preserving the local optimistic selection so the checkbox state doesn't mismatch. Risk: if callers relied on refetch always happening after require-all-only saves to reflect other server-side changes, that still happens as before since the flag is only set on software-selection failure.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟑 82 medium β€” react πŸ‘/πŸ‘Ž to teach the reviewer

if (shouldUpdateSoftware) {
Expand All @@ -170,6 +179,7 @@ const InstallSoftwareForm = ({
hadSuccess = true;
// Still let parent refetch even if the macOS call later fails
} catch (e) {
softwareUpdateFailed = true;
errorNotifications.push({
id: "update-software",
alertType: "error",
Expand Down Expand Up @@ -216,7 +226,14 @@ const InstallSoftwareForm = ({
renderFlash("success", "Successfully updated.");
}

refetchSoftwareTitles();
// Only refetch (which resyncs local selection state from server data via
// the effect above) if the software-selection save didn't fail. If it
// failed, refetching would discard the user's unsaved local selection
// and replace it with stale server data without any indication that the
// save didn't happen.
if (!softwareUpdateFailed) {
refetchSoftwareTitles();
}
setIsSaving(false);
};

Expand Down
Loading