-
Notifications
You must be signed in to change notification settings - Fork 1
fix(adhoc-sweep-fixes): CU-86akj32d7 74 review findings across 40 files #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2a0b082
dd6bc00
b570a99
7933bb9
7c2528b
4aa44bf
3edd0f0
1edcda0
0d14251
7feaf57
05128e8
30ea137
1c73ef2
1ac2181
ca9eaa7
40d7d16
7fe93dc
0ee5e96
41783eb
6f0e1fb
85946b1
130bcea
c5b1fed
ef2de1c
c640834
e48a2b6
15c3d2c
213488c
f5b80df
3a3e678
7d65a49
2452832
a72f0e6
4b72002
49ca57a
edd6e85
97078f0
a9527f1
09b1ccc
d22b8d4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,11 +5,13 @@ package main | |
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "log/slog" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "regexp" | ||
| "strings" | ||
| "time" | ||
|
|
||
|
|
@@ -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 | ||
| } | ||
|
|
@@ -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 := ` | ||
|
|
@@ -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 != "" { | ||
|
|
@@ -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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 π€ Prompt for AI agentsfix confidence: π‘ 80 medium β react π/π to teach the reviewer |
||
|
|
@@ -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 { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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})`) | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 π€ Prompt for AI agentsfix 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 | ||
|
|
@@ -246,7 +246,8 @@ func (imp *Importer) importEntry(entry SoftwareEntry) { | |
| if !imp.dryRun { | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 π€ Prompt for AI agentsfix 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) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,11 @@ | |
| APPDIR="/Applications/" | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 π€ Prompt for AI agentsfix confidence: π‘ 85 medium β react π/π to teach the reviewer |
||
| TMPDIR=$(mktemp -d) | ||
|
|
||
| cleanup() { | ||
| rm -rf "$TMPDIR" | ||
| } | ||
| trap cleanup EXIT | ||
|
|
||
| # functions | ||
|
|
||
| quit_application() { | ||
|
|
@@ -16,8 +21,8 @@ quit_application() { | |
| fi | ||
|
|
||
| local console_user | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 π€ Prompt for AI agentsfix 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 | ||
|
|
@@ -83,3 +88,4 @@ if [ $EXIT_CODE -ne 0 ]; then | |
| exit $EXIT_CODE | ||
| fi | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -175,7 +175,7 @@ def req(method: :get, path: '', body: nil, headers: {}, cached: false, environme | |
| end | ||
| end | ||
| rescue => e | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 π€ Prompt for AI agentsfix confidence: π’ 95 high β react π/π to teach the reviewer |
||
| out['error'] = e | ||
| out['error'] = e.message | ||
| end | ||
|
|
||
| out | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -365,7 +365,7 @@ export const SoftwareInstallDetailsModal = ({ | |
| const overrideFailedMessageWithInstalledMessage = | ||
| canOverrideFailureWithInstalled && | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 π€ Prompt for AI agentsfix 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) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,14 +76,28 @@ const rebuildQueryStringWithTeamId = ( | |
| parts.splice(pageIndex, 1, "page=0"); | ||
| } | ||
|
|
||
| // Backward compat: rewrite legacy team_id= to fleet_id= | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 π€ Prompt for AI agentsfix 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=")); | ||
|
|
@@ -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[]) => { | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 π€ Prompt for AI agentsfix 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]; | ||
| }; | ||
|
|
@@ -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) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 π€ Prompt for AI agentsfix confidence: π’ 100 high β react π/π to teach the reviewer |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -141,13 +141,34 @@ const BootstrapPackage = ({ | |
| const onDelete = async () => { | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 π€ Prompt for AI agentsfix confidence: π‘ 75 medium β react π/π to teach the reviewer
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
π€ Prompt for AI agentsfix 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(); | ||
|
|
||
| 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"; | ||
|
|
||
|
|
@@ -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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 π€ Prompt for AI agentsfix confidence: π‘ 78 medium β react π/π to teach the reviewer |
||
|
|
@@ -158,6 +166,7 @@ const InstallSoftwareForm = ({ | |
|
|
||
| const errorNotifications: INotification[] = []; | ||
| let hadSuccess = false; | ||
| let softwareUpdateFailed = false; | ||
|
|
||
| // 1. Software selection update | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 π€ Prompt for AI agentsfix confidence: π‘ 82 medium β react π/π to teach the reviewer |
||
| if (shouldUpdateSoftware) { | ||
|
|
@@ -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", | ||
|
|
@@ -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); | ||
| }; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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 newsqlIdentifierAllowlistregexp andvalidateSqlIdentifierStricthelper that restrictsappName,appPath, anduniqueIdentifierto 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 existingvalidateSqlInputblocklist 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 becauseosqueryi --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 toosqueryi, which is a larger, riskier change spanning how this command invokes osquery.π€ Prompt for AI agents
fix confidence: π΄ 55 low β review closely β react π/π to teach the reviewer