diff --git a/.custom-gcl.yml b/.custom-gcl.yml index 1b94d3d6ca..c34d5e5c15 100644 --- a/.custom-gcl.yml +++ b/.custom-gcl.yml @@ -5,7 +5,7 @@ version: v2.11.3 plugins: - module: "go.uber.org/nilaway" import: "go.uber.org/nilaway/cmd/gclplugin" - version: v0.0.0-20260126174828-99d94caaf043 # fixed version for reproducible builds - latest as of 2026-01-29 + version: v0.0.0-20260528182042-490362de4fb6 # fixed version for reproducible builds - latest as of 2026-06-01 - module: "github.com/fleetdm/fleet/v4/tools/ci/setboolcheck" import: "github.com/fleetdm/fleet/v4/tools/ci/setboolcheck/cmd/gclplugin" path: "tools/ci/setboolcheck" diff --git a/.golangci-incremental.yml b/.golangci-incremental.yml index aa5e12b82f..2454282258 100644 --- a/.golangci-incremental.yml +++ b/.golangci-incremental.yml @@ -56,10 +56,3 @@ linters: description: Flags json/url/query struct tags using deprecated "team"/"teams" and "query"/"queries" terms. exclusions: generated: strict - rules: - # nilaway has a hardcoded 500 CFG block limit (_maxFuncSizeInCFGBlocks). Functions exceeding - # it produce an INTERNAL ERROR with a bogus $GOROOT path that crashes golangci-lint's - # generated_file_filter processor. These are informational skip messages, not real findings. - - linters: - - nilaway - text: "INTERNAL ERROR" diff --git a/changes/46554-refactor-listhostsoftware-modifyappconfig-nilaway b/changes/46554-refactor-listhostsoftware-modifyappconfig-nilaway new file mode 100644 index 0000000000..f6e90ee9f7 --- /dev/null +++ b/changes/46554-refactor-listhostsoftware-modifyappconfig-nilaway @@ -0,0 +1 @@ +* Refactored `ListHostSoftware` and `ModifyAppConfig` into smaller helpers so nilaway can analyze them for nil-pointer dereferences. diff --git a/server/datastore/mysql/software.go b/server/datastore/mysql/software.go index 4c074b4da8..95f3c5be62 100644 --- a/server/datastore/mysql/software.go +++ b/server/datastore/mysql/software.go @@ -4603,6 +4603,365 @@ var hostSoftwareAllowedOrderKeys = common_mysql.OrderKeyAllowlist{ "source": "source", } +// hostSoftwareTitleAssembler accumulates and de-duplicates host software title records +// produced by ListHostSoftware. Its addRecord method holds the per-row merging logic +// extracted from the main function so the CFG block count stays under nilaway's limit. +type hostSoftwareTitleAssembler struct { + bySoftwareID map[uint]*hostSoftware + bySoftwareTitleID map[uint]*hostSoftware + byVPPAdamID map[string]*hostSoftware + byInHouseID map[uint]*hostSoftware + hostVPPInstalledTitles map[uint]*hostSoftware + hostInHouseInstalledTitles map[uint]*hostSoftware + hostInstalledSoftwareSet map[uint]*hostSoftware + filteredBySoftwareTitleID map[uint]*hostSoftware + filteredByVPPAdamID map[string]*hostSoftware + filteredByInHouseID map[uint]*hostSoftware + installedPathBySoftwareId map[uint][]string + pathSignatureInformation map[uint][]fleet.PathSignatureInformation + vulnerabilitiesBySoftwareID map[uint][]string + policiesBySoftwareTitleId map[uint][]fleet.AutomaticInstallPolicy + iconsBySoftwareTitleID map[uint]fleet.SoftwareTitleIcon + displayNames map[uint]string + + indexOfSoftwareTitle map[uint]uint + deduplicatedList []*hostSoftware +} + +func (a *hostSoftwareTitleAssembler) addRecord( + ctx context.Context, + ds *Datastore, + host *fleet.Host, + teamID uint, + softwareTitleRecord *hostSoftware, +) { + softwareTitle := a.bySoftwareTitleID[softwareTitleRecord.ID] + inventoriedVPPApp := a.hostVPPInstalledTitles[softwareTitleRecord.ID] + inventoriedInHouseApp := a.hostInHouseInstalledTitles[softwareTitleRecord.ID] + + 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 := a.hostInstalledSoftwareSet[*softwareTitle.SoftwareID]; ok { + softwareIDStr := strconv.FormatUint(uint64(*softwareTitle.SoftwareID), 10) + 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 := a.hostInstalledSoftwareSet[*inventoriedVPPApp.SoftwareID]; ok { + softwareIDStr := strconv.FormatUint(uint64(*inventoriedVPPApp.SoftwareID), 10) + pushVersion(softwareIDStr, softwareTitleRecord, *s) + } + } + 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 := a.hostInstalledSoftwareSet[*inventoriedInHouseApp.SoftwareID]; ok { + softwareIDStr := strconv.FormatUint(uint64(*inventoriedInHouseApp.SoftwareID), 10) + pushVersion(softwareIDStr, softwareTitleRecord, *s) + } + } + + if softwareTitleRecord.SoftwareIDList != nil { + softwareIDList := strings.Split(*softwareTitleRecord.SoftwareIDList, ",") + softwareSourceList := strings.Split(*softwareTitleRecord.SoftwareSourceList, ",") + softwareVersionList := strings.Split(*softwareTitleRecord.VersionList, ",") + softwareBundleIdentifierList := strings.Split(*softwareTitleRecord.BundleIdentifierList, ",") + + for index, softwareIdStr := range softwareIDList { + version := &fleet.HostSoftwareInstalledVersion{} + + if softwareId, err := strconv.ParseUint(softwareIdStr, 10, 32); err == nil { + + softwareId := uint(softwareId) + if software, ok := a.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 = a.installedPathBySoftwareId[softwareId] + version.Vulnerabilities = a.vulnerabilitiesBySoftwareID[softwareId] + + if version.Source == "apps" { + version.SignatureInformation = a.pathSignatureInformation[softwareId] + } + + if storedIndex, ok := a.indexOfSoftwareTitle[softwareTitleRecord.ID]; ok { + a.deduplicatedList[storedIndex].InstalledVersions = append(a.deduplicatedList[storedIndex].InstalledVersions, version) + } else { + softwareTitleRecord.InstalledVersions = append(softwareTitleRecord.InstalledVersions, version) + } + } + } + } + } + + 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 := a.indexOfSoftwareTitle[softwareTitleRecord.ID]; ok { + softwareTitleRecord = a.deduplicatedList[storedIndex] + } + + for index, vppAppAdamIdStr := range vppAppAdamIDList { + if vppAppAdamIdStr != "" { + softwareTitle = a.byVPPAdamID[vppAppAdamIdStr] + softwareTitleRecord.VPPAppAdamID = &vppAppAdamIdStr + } + + vppAppSelfService := vppAppSelfServiceList[index] + if vppAppSelfService != "" { + if vppAppSelfService == "1" { + softwareTitleRecord.VPPAppSelfService = new(true) + } else { + softwareTitleRecord.VPPAppSelfService = new(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 + } + } + } + + if softwareTitleRecord.InHouseAppIDList != nil { + inHouseAppIDList := strings.Split(*softwareTitleRecord.InHouseAppIDList, ",") + inHouseAppVersionList := strings.Split(*softwareTitleRecord.InHouseAppVersionList, ",") + inHouseAppPlatformList := strings.Split(*softwareTitleRecord.InHouseAppPlatformList, ",") + inHouseAppNameList := strings.Split(*softwareTitleRecord.InHouseAppNameList, ",") + inHouseAppSelfServiceList := strings.Split(*softwareTitleRecord.InHouseAppSelfServiceList, ",") + + if storedIndex, ok := a.indexOfSoftwareTitle[softwareTitleRecord.ID]; ok { + softwareTitleRecord = a.deduplicatedList[storedIndex] + } + + for index, inHouseAppIDStr := range inHouseAppIDList { + inHouseID64, err := strconv.ParseUint(inHouseAppIDStr, 10, 32) + if err != nil { + continue + } + + inHouseID := uint(inHouseID64) + + softwareTitle = a.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 + } + inHouseAppSelfService := inHouseAppSelfServiceList[index] + if inHouseAppSelfService != "" { + if inHouseAppSelfService == "1" { + softwareTitleRecord.InHouseAppSelfService = new(true) + } else { + softwareTitleRecord.InHouseAppSelfService = new(false) + } + } + } + } + + if storedIndex, ok := a.indexOfSoftwareTitle[softwareTitleRecord.ID]; ok { + softwareTitleRecord = a.deduplicatedList[storedIndex] + } + + // Merge the data of `software title` into `softwareTitleRecord` + // We should try to move as much of these attributes into the `stmt` query + 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 + } + } + + // promote the package name and version to the proper destination fields + if softwareTitleRecord.PackageName != nil { + if _, ok := a.filteredBySoftwareTitleID[softwareTitleRecord.ID]; ok { + hydrateHostSoftwareRecordFromDb(softwareTitleRecord, softwareTitle) + } + } + // Here and below: populate LastInstall for software packages, VPP apps, and in-house apps + // even if installer is out of scope so failed install attempts show the execution ID for viewing details. + if softwareTitleRecord.SoftwarePackage != nil && softwareTitleRecord.SoftwarePackage.LastInstall == nil { + if softwareTitle != nil && softwareTitle.LastInstallInstallUUID != nil && *softwareTitle.LastInstallInstallUUID != "" { + softwareTitleRecord.SoftwarePackage.LastInstall = &fleet.HostSoftwareInstall{ + InstallUUID: *softwareTitle.LastInstallInstallUUID, + } + if softwareTitle.LastInstallInstalledAt != nil { + softwareTitleRecord.SoftwarePackage.LastInstall.InstalledAt = *softwareTitle.LastInstallInstalledAt + } + } + } + // Populate LastUninstall for software packages even if installer is out of scope. + if softwareTitleRecord.SoftwarePackage != nil && softwareTitleRecord.SoftwarePackage.LastUninstall == nil { + if softwareTitle != nil && softwareTitle.LastUninstallScriptExecutionID != nil && *softwareTitle.LastUninstallScriptExecutionID != "" { + softwareTitleRecord.SoftwarePackage.LastUninstall = &fleet.HostSoftwareUninstall{ + ExecutionID: *softwareTitle.LastUninstallScriptExecutionID, + } + if softwareTitle.LastUninstallUninstalledAt != nil { + softwareTitleRecord.SoftwarePackage.LastUninstall.UninstalledAt = *softwareTitle.LastUninstallUninstalledAt + } + } + } + + // 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 := a.hostVPPInstalledTitles[softwareTitleRecord.ID]; ok { + softwareTitleRecord.VPPAppAdamID = installedVppRecord.VPPAppAdamID + softwareTitleRecord.VPPAppVersion = installedVppRecord.VPPAppVersion + softwareTitleRecord.VPPAppPlatform = installedVppRecord.VPPAppPlatform + softwareTitleRecord.VPPAppIconURL = installedVppRecord.VPPAppIconURL + softwareTitleRecord.VPPAppSelfService = installedVppRecord.VPPAppSelfService + } + // promote the VPP app id and version to the proper destination fields + if softwareTitleRecord.VPPAppAdamID != nil { + if _, ok := a.filteredByVPPAdamID[*softwareTitleRecord.VPPAppAdamID]; ok { + promoteSoftwareTitleVPPApp(softwareTitleRecord) + } + } + if softwareTitleRecord.AppStoreApp != nil && softwareTitleRecord.AppStoreApp.LastInstall == nil { + if softwareTitle != nil && softwareTitle.LastInstallInstallUUID != nil && *softwareTitle.LastInstallInstallUUID != "" { + softwareTitleRecord.AppStoreApp.LastInstall = &fleet.HostSoftwareInstall{ + CommandUUID: *softwareTitle.LastInstallInstallUUID, + } + if softwareTitle.LastInstallInstalledAt != nil { + softwareTitleRecord.AppStoreApp.LastInstall.InstalledAt = *softwareTitle.LastInstallInstalledAt + } + } + } + + // 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 := a.hostInHouseInstalledTitles[softwareTitleRecord.ID]; ok { + softwareTitleRecord.InHouseAppID = installedInHouseRecord.InHouseAppID + softwareTitleRecord.InHouseAppName = installedInHouseRecord.InHouseAppName + softwareTitleRecord.InHouseAppVersion = installedInHouseRecord.InHouseAppVersion + softwareTitleRecord.InHouseAppPlatform = installedInHouseRecord.InHouseAppPlatform + softwareTitleRecord.InHouseAppSelfService = installedInHouseRecord.InHouseAppSelfService + } + // promote the in-house app id and version to the proper destination fields + if softwareTitleRecord.InHouseAppID != nil { + if _, ok := a.filteredByInHouseID[*softwareTitleRecord.InHouseAppID]; ok { + promoteSoftwareTitleInHouseApp(softwareTitleRecord) + } + } + // N.b., in-house apps use SoftwarePackage struct with CommandUUID. + if softwareTitleRecord.SoftwarePackage != nil && softwareTitleRecord.InHouseAppID != nil && softwareTitleRecord.SoftwarePackage.LastInstall == nil { + if softwareTitle != nil && softwareTitle.LastInstallInstallUUID != nil && *softwareTitle.LastInstallInstallUUID != "" { + softwareTitleRecord.SoftwarePackage.LastInstall = &fleet.HostSoftwareInstall{ + CommandUUID: *softwareTitle.LastInstallInstallUUID, + } + if softwareTitle.LastInstallInstalledAt != nil { + softwareTitleRecord.SoftwarePackage.LastInstall.InstalledAt = *softwareTitle.LastInstallInstalledAt + } + } + } + + // NOTE: in-house apps do not support automatic install policies at the moment + if policies, ok := a.policiesBySoftwareTitleId[softwareTitleRecord.ID]; ok { + switch { + case softwareTitleRecord.AppStoreApp != nil: + softwareTitleRecord.AppStoreApp.AutomaticInstallPolicies = policies + case softwareTitleRecord.SoftwarePackage != nil: + softwareTitleRecord.SoftwarePackage.AutomaticInstallPolicies = policies + default: + ds.logger.WarnContext(ctx, "software title record should have an associated VPP application or software package", + "team_id", teamID, + "host_id", host.ID, + "software_title_id", softwareTitleRecord.ID, + ) + } + } + + if icon, ok := a.iconsBySoftwareTitleID[softwareTitleRecord.ID]; ok { + softwareTitleRecord.IconUrl = new(icon.IconUrl()) + } + + if displayName, ok := a.displayNames[softwareTitleRecord.ID]; ok { + softwareTitleRecord.DisplayName = displayName + } + + if _, ok := a.indexOfSoftwareTitle[softwareTitleRecord.ID]; !ok { + a.indexOfSoftwareTitle[softwareTitleRecord.ID] = uint(len(a.deduplicatedList)) + a.deduplicatedList = append(a.deduplicatedList, softwareTitleRecord) + } +} + +// filterOutOfScopeFailedHostSoftwareInstalls removes failed install entries that are not in +// the osquery inventory and whose installer is out of label scope, so they don't surface +// as available software on the host. Maps are mutated in place. +func filterOutOfScopeFailedHostSoftwareInstalls( + bySoftwareTitleID map[uint]*hostSoftware, + byVPPAdamID map[string]*hostSoftware, + byInHouseID map[uint]*hostSoftware, + hostInstalledSoftwareTitleSet map[uint]struct{}, + filteredBySoftwareTitleID map[uint]*hostSoftware, + filteredByVPPAdamID map[string]*hostSoftware, + filteredByInHouseID map[uint]*hostSoftware, +) { + for titleID, st := range bySoftwareTitleID { + if st.InstallerID != nil { + if _, isInstalled := hostInstalledSoftwareTitleSet[titleID]; !isInstalled { + if st.Status != nil && *st.Status == fleet.SoftwareInstallFailed { + if _, inScope := filteredBySoftwareTitleID[titleID]; !inScope { + delete(bySoftwareTitleID, titleID) + } + } + } + } + } + for adamID, st := range byVPPAdamID { + if _, isInstalled := hostInstalledSoftwareTitleSet[st.ID]; !isInstalled { + if st.Status != nil && *st.Status == fleet.SoftwareInstallFailed { + if _, inScope := filteredByVPPAdamID[adamID]; !inScope { + delete(byVPPAdamID, adamID) + } + } + } + } + for appID, st := range byInHouseID { + if _, isInstalled := hostInstalledSoftwareTitleSet[st.ID]; !isInstalled { + if st.Status != nil && *st.Status == fleet.SoftwareInstallFailed { + if _, inScope := filteredByInHouseID[appID]; !inScope { + delete(byInHouseID, appID) + } + } + } + } +} + 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( @@ -5566,39 +5925,15 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opt } } - // Filter out-of-scope FAILED installs from all app types. - // Only remove if not in inventory AND status is failed AND out of label scope. - for titleID, st := range bySoftwareTitleID { - if st.InstallerID != nil { - // Check if software is NOT actually installed (not in osquery inventory) - if _, isInstalled := hostInstalledSoftwareTitleSet[titleID]; !isInstalled { - // Only remove if install FAILED and installer is out of scope - if st.Status != nil && *st.Status == fleet.SoftwareInstallFailed { - if _, inScope := filteredBySoftwareTitleID[titleID]; !inScope { - delete(bySoftwareTitleID, titleID) - } - } - } - } - } - for adamID, st := range byVPPAdamID { - if _, isInstalled := hostInstalledSoftwareTitleSet[st.ID]; !isInstalled { - if st.Status != nil && *st.Status == fleet.SoftwareInstallFailed { - if _, inScope := filteredByVPPAdamID[adamID]; !inScope { - delete(byVPPAdamID, adamID) - } - } - } - } - for appID, st := range byInHouseID { - if _, isInstalled := hostInstalledSoftwareTitleSet[st.ID]; !isInstalled { - if st.Status != nil && *st.Status == fleet.SoftwareInstallFailed { - if _, inScope := filteredByInHouseID[appID]; !inScope { - delete(byInHouseID, appID) - } - } - } - } + filterOutOfScopeFailedHostSoftwareInstalls( + bySoftwareTitleID, + byVPPAdamID, + byInHouseID, + hostInstalledSoftwareTitleSet, + filteredBySoftwareTitleID, + filteredByVPPAdamID, + filteredByInHouseID, + ) if opts.OnlyAvailableForInstall { bySoftwareTitleID = filteredBySoftwareTitleID @@ -6154,294 +6489,30 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opt return nil, nil, ctxerr.Wrap(ctx, err, "get software display names by team and title IDs") } - indexOfSoftwareTitle := make(map[uint]uint) - deduplicatedList := make([]*hostSoftware, 0, len(hostSoftwareList)) - for _, softwareTitleRecord := range hostSoftwareList { - softwareTitle := bySoftwareTitleID[softwareTitleRecord.ID] - inventoriedVPPApp := hostVPPInstalledTitles[softwareTitleRecord.ID] - inventoriedInHouseApp := hostInHouseInstalledTitles[softwareTitleRecord.ID] - - 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) - 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) - } - } - 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) - } - } - - if softwareTitleRecord.SoftwareIDList != nil { - softwareIDList := strings.Split(*softwareTitleRecord.SoftwareIDList, ",") - softwareSourceList := strings.Split(*softwareTitleRecord.SoftwareSourceList, ",") - softwareVersionList := strings.Split(*softwareTitleRecord.VersionList, ",") - softwareBundleIdentifierList := strings.Split(*softwareTitleRecord.BundleIdentifierList, ",") - - 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) - } - } - } - } - } - - 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] - softwareTitleRecord.VPPAppAdamID = &vppAppAdamIdStr - } - - 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 - } - } - } - - if softwareTitleRecord.InHouseAppIDList != nil { - inHouseAppIDList := strings.Split(*softwareTitleRecord.InHouseAppIDList, ",") - inHouseAppVersionList := strings.Split(*softwareTitleRecord.InHouseAppVersionList, ",") - inHouseAppPlatformList := strings.Split(*softwareTitleRecord.InHouseAppPlatformList, ",") - inHouseAppNameList := strings.Split(*softwareTitleRecord.InHouseAppNameList, ",") - inHouseAppSelfServiceList := strings.Split(*softwareTitleRecord.InHouseAppSelfServiceList, ",") - - 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 - } - inHouseAppSelfService := inHouseAppSelfServiceList[index] - if inHouseAppSelfService != "" { - if inHouseAppSelfService == "1" { - softwareTitleRecord.InHouseAppSelfService = ptr.Bool(true) - } else { - softwareTitleRecord.InHouseAppSelfService = ptr.Bool(false) - } - } - } - } - - if storedIndex, ok := indexOfSoftwareTitle[softwareTitleRecord.ID]; ok { - softwareTitleRecord = deduplicatedList[storedIndex] - } - - // Merge the data of `software title` into `softwareTitleRecord` - // We should try to move as much of these attributes into the `stmt` query - 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 - } - } - - // promote the package name and version to the proper destination fields - if softwareTitleRecord.PackageName != nil { - if _, ok := filteredBySoftwareTitleID[softwareTitleRecord.ID]; ok { - hydrateHostSoftwareRecordFromDb(softwareTitleRecord, softwareTitle) - } - } - // Here and below: populate LastInstall for software packages, VPP apps, and in-house apps - // even if installer is out of scope so failed install attempts show the execution ID for viewing details. - if softwareTitleRecord.SoftwarePackage != nil && softwareTitleRecord.SoftwarePackage.LastInstall == nil { - if softwareTitle != nil && softwareTitle.LastInstallInstallUUID != nil && *softwareTitle.LastInstallInstallUUID != "" { - softwareTitleRecord.SoftwarePackage.LastInstall = &fleet.HostSoftwareInstall{ - InstallUUID: *softwareTitle.LastInstallInstallUUID, - } - if softwareTitle.LastInstallInstalledAt != nil { - softwareTitleRecord.SoftwarePackage.LastInstall.InstalledAt = *softwareTitle.LastInstallInstalledAt - } - } - } - // Populate LastUninstall for software packages even if installer is out of scope. - if softwareTitleRecord.SoftwarePackage != nil && softwareTitleRecord.SoftwarePackage.LastUninstall == nil { - if softwareTitle != nil && softwareTitle.LastUninstallScriptExecutionID != nil && *softwareTitle.LastUninstallScriptExecutionID != "" { - softwareTitleRecord.SoftwarePackage.LastUninstall = &fleet.HostSoftwareUninstall{ - ExecutionID: *softwareTitle.LastUninstallScriptExecutionID, - } - if softwareTitle.LastUninstallUninstalledAt != nil { - softwareTitleRecord.SoftwarePackage.LastUninstall.UninstalledAt = *softwareTitle.LastUninstallUninstalledAt - } - } - } - - // 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 - } - // promote the VPP app id and version to the proper destination fields - if softwareTitleRecord.VPPAppAdamID != nil { - if _, ok := filteredByVPPAdamID[*softwareTitleRecord.VPPAppAdamID]; ok { - promoteSoftwareTitleVPPApp(softwareTitleRecord) - } - } - if softwareTitleRecord.AppStoreApp != nil && softwareTitleRecord.AppStoreApp.LastInstall == nil { - if softwareTitle != nil && softwareTitle.LastInstallInstallUUID != nil && *softwareTitle.LastInstallInstallUUID != "" { - softwareTitleRecord.AppStoreApp.LastInstall = &fleet.HostSoftwareInstall{ - CommandUUID: *softwareTitle.LastInstallInstallUUID, - } - if softwareTitle.LastInstallInstalledAt != nil { - softwareTitleRecord.AppStoreApp.LastInstall.InstalledAt = *softwareTitle.LastInstallInstalledAt - } - } - } - - // 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 - softwareTitleRecord.InHouseAppSelfService = installedInHouseRecord.InHouseAppSelfService - } - // 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) - } - } - // N.b., in-house apps use SoftwarePackage struct with CommandUUID. - if softwareTitleRecord.SoftwarePackage != nil && softwareTitleRecord.InHouseAppID != nil && softwareTitleRecord.SoftwarePackage.LastInstall == nil { - if softwareTitle != nil && softwareTitle.LastInstallInstallUUID != nil && *softwareTitle.LastInstallInstallUUID != "" { - softwareTitleRecord.SoftwarePackage.LastInstall = &fleet.HostSoftwareInstall{ - CommandUUID: *softwareTitle.LastInstallInstallUUID, - } - if softwareTitle.LastInstallInstalledAt != nil { - softwareTitleRecord.SoftwarePackage.LastInstall.InstalledAt = *softwareTitle.LastInstallInstalledAt - } - } - } - - // NOTE: in-house apps do not support automatic install policies at the moment - if policies, ok := policiesBySoftwareTitleId[softwareTitleRecord.ID]; ok { - switch { - case softwareTitleRecord.AppStoreApp != nil: - softwareTitleRecord.AppStoreApp.AutomaticInstallPolicies = policies - case softwareTitleRecord.SoftwarePackage != nil: - softwareTitleRecord.SoftwarePackage.AutomaticInstallPolicies = policies - default: - ds.logger.WarnContext(ctx, "software title record should have an associated VPP application or software package", - "team_id", teamID, - "host_id", host.ID, - "software_title_id", softwareTitleRecord.ID, - ) - } - } - - if icon, ok := iconsBySoftwareTitleID[softwareTitleRecord.ID]; ok { - softwareTitleRecord.IconUrl = ptr.String(icon.IconUrl()) - } - - if displayName, ok := displayNames[softwareTitleRecord.ID]; ok { - softwareTitleRecord.DisplayName = displayName - } - - if _, ok := indexOfSoftwareTitle[softwareTitleRecord.ID]; !ok { - indexOfSoftwareTitle[softwareTitleRecord.ID] = uint(len(deduplicatedList)) - deduplicatedList = append(deduplicatedList, softwareTitleRecord) - } + assembler := &hostSoftwareTitleAssembler{ + bySoftwareID: bySoftwareID, + bySoftwareTitleID: bySoftwareTitleID, + byVPPAdamID: byVPPAdamID, + byInHouseID: byInHouseID, + hostVPPInstalledTitles: hostVPPInstalledTitles, + hostInHouseInstalledTitles: hostInHouseInstalledTitles, + hostInstalledSoftwareSet: hostInstalledSoftwareSet, + filteredBySoftwareTitleID: filteredBySoftwareTitleID, + filteredByVPPAdamID: filteredByVPPAdamID, + filteredByInHouseID: filteredByInHouseID, + installedPathBySoftwareId: installedPathBySoftwareId, + pathSignatureInformation: pathSignatureInformation, + vulnerabilitiesBySoftwareID: vulnerabilitiesBySoftwareID, + policiesBySoftwareTitleId: policiesBySoftwareTitleId, + iconsBySoftwareTitleID: iconsBySoftwareTitleID, + displayNames: displayNames, + indexOfSoftwareTitle: make(map[uint]uint), + deduplicatedList: make([]*hostSoftware, 0, len(hostSoftwareList)), } - - hostSoftwareList = deduplicatedList + for _, softwareTitleRecord := range hostSoftwareList { + assembler.addRecord(ctx, ds, host, teamID, softwareTitleRecord) + } + hostSoftwareList = assembler.deduplicatedList } perPage := opts.ListOptions.PerPage diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 5e61081046..b856637f0c 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -345,6 +345,135 @@ func modifyAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet return response, nil } +// applyAndValidateConditionalAccessOktaFields merges incoming Okta conditional-access fields +// into appConfig and validates the combination. Extracted from ModifyAppConfig so the parent +// function's CFG block count stays under nilaway's limit. +func applyAndValidateConditionalAccessOktaFields( + ctx context.Context, + appConfig *fleet.AppConfig, + newAppConfig *fleet.AppConfig, + invalid *fleet.InvalidArgumentError, + lic *fleet.LicenseInfo, +) error { + if appConfig.ConditionalAccess == nil { + appConfig.ConditionalAccess = &fleet.ConditionalAccessSettings{} + } + if newAppConfig.ConditionalAccess == nil { + newAppConfig.ConditionalAccess = &fleet.ConditionalAccessSettings{} + } + + // Normalize incoming Okta fields (trim whitespace) BEFORE the premium-license gate so a + // whitespace-only input that would persist as empty does not trip the license check. + normalizeOptString := func(src optjson.String) optjson.String { + if src.Set && src.Valid { + src.Value = strings.TrimSpace(src.Value) + } + return src + } + applyOptString := func(dest *optjson.String, src optjson.String) { + if src.Set { + *dest = src + } + } + oktaIDPID := normalizeOptString(newAppConfig.ConditionalAccess.OktaIDPID) + oktaACSURL := normalizeOptString(newAppConfig.ConditionalAccess.OktaAssertionConsumerServiceURL) + oktaAudienceURI := normalizeOptString(newAppConfig.ConditionalAccess.OktaAudienceURI) + oktaCert := normalizeOptString(newAppConfig.ConditionalAccess.OktaCertificate) + applyOptString(&appConfig.ConditionalAccess.OktaIDPID, oktaIDPID) + applyOptString(&appConfig.ConditionalAccess.OktaAssertionConsumerServiceURL, oktaACSURL) + applyOptString(&appConfig.ConditionalAccess.OktaAudienceURI, oktaAudienceURI) + applyOptString(&appConfig.ConditionalAccess.OktaCertificate, oktaCert) + + isNonEmpty := func(s optjson.String) bool { + return s.Set && s.Valid && s.Value != "" + } + oktaFieldsBeingSet := isNonEmpty(oktaIDPID) || + isNonEmpty(oktaACSURL) || + isNonEmpty(oktaAudienceURI) || + isNonEmpty(oktaCert) + + if oktaFieldsBeingSet && !lic.IsPremium() { + invalid.Append("conditional_access", ErrMissingLicense.Error()) + return ctxerr.Wrap(ctx, invalid) + } + + oktaFieldsSet := 0 + if appConfig.ConditionalAccess.OktaIDPID.Valid && appConfig.ConditionalAccess.OktaIDPID.Value != "" { + oktaFieldsSet++ + } + if appConfig.ConditionalAccess.OktaAssertionConsumerServiceURL.Valid && + appConfig.ConditionalAccess.OktaAssertionConsumerServiceURL.Value != "" { + oktaFieldsSet++ + } + if appConfig.ConditionalAccess.OktaAudienceURI.Valid && + appConfig.ConditionalAccess.OktaAudienceURI.Value != "" { + oktaFieldsSet++ + } + if appConfig.ConditionalAccess.OktaCertificate.Valid && + appConfig.ConditionalAccess.OktaCertificate.Value != "" { + oktaFieldsSet++ + } + + if oktaFieldsSet > 0 && oktaFieldsSet < 4 { + invalid.Append("conditional_access", + "all Okta fields must be set together (okta_idp_id, okta_assertion_consumer_service_url, okta_audience_uri, okta_certificate) or all must be empty") + } + + if oktaFieldsSet == 4 { + const ( + maxURLLength = 2048 + maxCertLength = 8192 + ) + + if len(appConfig.ConditionalAccess.OktaIDPID.Value) > maxURLLength { + invalid.Append("conditional_access.okta_idp_id", + fmt.Sprintf("must be %d characters or less", maxURLLength)) + } + if len(appConfig.ConditionalAccess.OktaAssertionConsumerServiceURL.Value) > maxURLLength { + invalid.Append("conditional_access.okta_assertion_consumer_service_url", + fmt.Sprintf("must be %d characters or less", maxURLLength)) + } + if len(appConfig.ConditionalAccess.OktaAudienceURI.Value) > maxURLLength { + invalid.Append("conditional_access.okta_audience_uri", + fmt.Sprintf("must be %d characters or less", maxURLLength)) + } + if len(appConfig.ConditionalAccess.OktaCertificate.Value) > maxCertLength { + invalid.Append("conditional_access.okta_certificate", + fmt.Sprintf("must be %d characters or less", maxCertLength)) + } + + acsURL, err := url.ParseRequestURI(appConfig.ConditionalAccess.OktaAssertionConsumerServiceURL.Value) + if err != nil || ((acsURL.Scheme != "http" && acsURL.Scheme != "https") || acsURL.Host == "") { + invalid.Append("conditional_access.okta_assertion_consumer_service_url", + "must be a valid URL with http or https scheme and a host") + } + + rest := []byte(appConfig.ConditionalAccess.OktaCertificate.Value) + certCount := 0 + for { + block, r := pem.Decode(rest) + if block == nil { + break + } + rest = r + if block.Type != "CERTIFICATE" { + invalid.Append("conditional_access.okta_certificate", "PEM block must be a CERTIFICATE") + break + } + if _, err := x509.ParseCertificate(block.Bytes); err != nil { + invalid.Append("conditional_access.okta_certificate", "must be a valid x509 certificate") + break + } + certCount++ + } + if certCount == 0 { + invalid.Append("conditional_access.okta_certificate", "must contain at least one PEM-encoded certificate") + } + } + + return nil +} + func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fleet.ApplySpecOptions) (*fleet.AppConfig, error) { if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionWrite); err != nil { return nil, err @@ -674,120 +803,8 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle fleet.ValidateEnabledHostStatusIntegrations(appConfig.WebhookSettings.HostStatusWebhook, invalid) fleet.ValidateEnabledActivitiesWebhook(appConfig.WebhookSettings.ActivitiesWebhook, invalid) - // Initialize ConditionalAccess if nil (it's a pointer type) - if appConfig.ConditionalAccess == nil { - appConfig.ConditionalAccess = &fleet.ConditionalAccessSettings{} - } - if newAppConfig.ConditionalAccess == nil { - newAppConfig.ConditionalAccess = &fleet.ConditionalAccessSettings{} - } - - // Trim whitespace from all Okta fields before setting them - applyOptString := func(dest *optjson.String, src optjson.String) { - if src.Set { - if src.Valid { - src.Value = strings.TrimSpace(src.Value) - } - *dest = src - } - } - applyOptString(&appConfig.ConditionalAccess.OktaIDPID, newAppConfig.ConditionalAccess.OktaIDPID) - applyOptString(&appConfig.ConditionalAccess.OktaAssertionConsumerServiceURL, newAppConfig.ConditionalAccess.OktaAssertionConsumerServiceURL) - applyOptString(&appConfig.ConditionalAccess.OktaAudienceURI, newAppConfig.ConditionalAccess.OktaAudienceURI) - applyOptString(&appConfig.ConditionalAccess.OktaCertificate, newAppConfig.ConditionalAccess.OktaCertificate) - // Handle Okta conditional access fields - only update if Set=true (partial update support) - // Check if any Okta fields are being set with valid (non-null) non-empty values - isNonEmpty := func(s optjson.String) bool { - return s.Set && s.Valid && s.Value != "" - } - oktaFieldsBeingSet := isNonEmpty(newAppConfig.ConditionalAccess.OktaIDPID) || - isNonEmpty(newAppConfig.ConditionalAccess.OktaAssertionConsumerServiceURL) || - isNonEmpty(newAppConfig.ConditionalAccess.OktaAudienceURI) || - isNonEmpty(newAppConfig.ConditionalAccess.OktaCertificate) - - if oktaFieldsBeingSet && !lic.IsPremium() { - invalid.Append("conditional_access", ErrMissingLicense.Error()) - return nil, ctxerr.Wrap(ctx, invalid) - } - - // Validate Okta configuration - all fields must be set together or all must be empty - oktaFieldsSet := 0 - if appConfig.ConditionalAccess.OktaIDPID.Valid && appConfig.ConditionalAccess.OktaIDPID.Value != "" { - oktaFieldsSet++ - } - if appConfig.ConditionalAccess.OktaAssertionConsumerServiceURL.Valid && - appConfig.ConditionalAccess.OktaAssertionConsumerServiceURL.Value != "" { - oktaFieldsSet++ - } - if appConfig.ConditionalAccess.OktaAudienceURI.Valid && - appConfig.ConditionalAccess.OktaAudienceURI.Value != "" { - oktaFieldsSet++ - } - if appConfig.ConditionalAccess.OktaCertificate.Valid && - appConfig.ConditionalAccess.OktaCertificate.Value != "" { - oktaFieldsSet++ - } - - // Either all 4 fields should be set, or none should be set - if oktaFieldsSet > 0 && oktaFieldsSet < 4 { - invalid.Append("conditional_access", - "all Okta fields must be set together (okta_idp_id, okta_assertion_consumer_service_url, okta_audience_uri, okta_certificate) or all must be empty") - } - - // If all fields are set, validate them - if oktaFieldsSet == 4 { - // Validate max lengths for Okta fields - const ( - maxURLLength = 2048 // Standard max URL length supported by browsers - maxCertLength = 8192 // 8KB for PEM certificate (without private key) - ) - - if len(appConfig.ConditionalAccess.OktaIDPID.Value) > maxURLLength { - invalid.Append("conditional_access.okta_idp_id", - fmt.Sprintf("must be %d characters or less", maxURLLength)) - } - if len(appConfig.ConditionalAccess.OktaAssertionConsumerServiceURL.Value) > maxURLLength { - invalid.Append("conditional_access.okta_assertion_consumer_service_url", - fmt.Sprintf("must be %d characters or less", maxURLLength)) - } - if len(appConfig.ConditionalAccess.OktaAudienceURI.Value) > maxURLLength { - invalid.Append("conditional_access.okta_audience_uri", - fmt.Sprintf("must be %d characters or less", maxURLLength)) - } - if len(appConfig.ConditionalAccess.OktaCertificate.Value) > maxCertLength { - invalid.Append("conditional_access.okta_certificate", - fmt.Sprintf("must be %d characters or less", maxCertLength)) - } - - // Validate URL format for ACS URL - must have http or https scheme and a host - acsURL, err := url.ParseRequestURI(appConfig.ConditionalAccess.OktaAssertionConsumerServiceURL.Value) - if err != nil || ((acsURL.Scheme != "http" && acsURL.Scheme != "https") || acsURL.Host == "") { - invalid.Append("conditional_access.okta_assertion_consumer_service_url", - "must be a valid URL with http or https scheme and a host") - } - - // Validate one or more PEM-encoded CERTIFICATE blocks and parse each - rest := []byte(appConfig.ConditionalAccess.OktaCertificate.Value) - certCount := 0 - for { - block, r := pem.Decode(rest) - if block == nil { - break - } - rest = r - if block.Type != "CERTIFICATE" { - invalid.Append("conditional_access.okta_certificate", "PEM block must be a CERTIFICATE") - break - } - if _, err := x509.ParseCertificate(block.Bytes); err != nil { - invalid.Append("conditional_access.okta_certificate", "must be a valid x509 certificate") - break - } - certCount++ - } - if certCount == 0 { - invalid.Append("conditional_access.okta_certificate", "must contain at least one PEM-encoded certificate") - } + if err := applyAndValidateConditionalAccessOktaFields(ctx, appConfig, &newAppConfig, invalid, lic); err != nil { + return nil, err } var conditionalAccessNoTeamUpdated bool