fleet/server/fleet/software.go
Jahziel Villasana-Espinoza 1cf16f0539
software display names: DB changes (#35066)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #33776 

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

## Database migrations

- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
2025-11-04 10:04:42 -05:00

611 lines
25 KiB
Go

package fleet
import (
"crypto/md5" //nolint:gosec // This hash is used as a DB optimization for software row lookup, not security
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
"unicode/utf8"
)
const (
SoftwareVendorMaxLengthFmt = "%.111s..."
SoftwareFieldSeparator = "\u0000"
//
// The following length values must be kept in sync with the DB column definitions.
//
SoftwareNameMaxLength = 255
SoftwareVersionMaxLength = 255
SoftwareSourceMaxLength = 64
SoftwareBundleIdentifierMaxLength = 255
SoftwareExtensionIDMaxLength = 255
SoftwareExtensionForMaxLength = 255
SoftwareReleaseMaxLength = 64
SoftwareVendorMaxLength = 114
SoftwareArchMaxLength = 16
// SoftwareTeamIdentifierMaxLength is the max length for Apple's Team ID,
// see https://developer.apple.com/help/account/manage-your-team/locate-your-team-id
SoftwareTeamIdentifierMaxLength = 10
)
type Vulnerabilities []CVE
// Software is a named and versioned piece of software installed on a device.
type Software struct {
ID uint `json:"id" db:"id"`
// Name is the reported name.
Name string `json:"name" db:"name"`
// Version is reported version.
Version string `json:"version" db:"version"`
// BundleIdentifier is the CFBundleIdentifier label from the info properties
BundleIdentifier string `json:"bundle_identifier,omitempty" db:"bundle_identifier"`
// Source is the source of the data (osquery table name).
Source string `json:"source" db:"source"`
// ExtensionID is the browser extension id (from osquery chrome_extensions and firefox_addons)
ExtensionID string `json:"extension_id,omitempty" db:"extension_id"`
// ExtensionFor is the host software that this software is an extension for
ExtensionFor string `json:"extension_for" db:"extension_for"`
// Browser is the browser type this extension is for (deprecated, use extension_for instead)
Browser string `json:"browser"`
// Release is the version of the OS this software was released on
// (e.g. "30.el7" for a CentOS package).
Release string `json:"release,omitempty" db:"release"`
// Vendor is the supplier of the software (e.g. "CentOS").
Vendor string `json:"vendor,omitempty" db:"vendor"`
// TODO: Remove this as part of the clean up of https://github.com/fleetdm/fleet/pull/7297
// DO NOT USE THIS, use 'Vendor' instead. We had to 'recreate' the vendor column because we
// needed to make it wider - the old column was left and renamed to 'vendor_old'
VendorOld string `json:"-" db:"vendor_old"`
// Arch is the architecture of the software (e.g. "x86_64").
Arch string `json:"arch,omitempty" db:"arch"`
// GenerateCPE is the CPE23 string that corresponds to the current software
GenerateCPE string `json:"generated_cpe" db:"generated_cpe"`
// Vulnerabilities lists all found vulnerablities
Vulnerabilities Vulnerabilities `json:"vulnerabilities"`
// HostsCount indicates the number of hosts with that software, filled only
// if explicitly requested.
HostsCount int `json:"hosts_count,omitempty" db:"hosts_count"`
// CountsUpdatedAt is the timestamp when the hosts count was last updated
// for that software, filled only if hosts count is requested.
CountsUpdatedAt time.Time `json:"-" db:"counts_updated_at"`
// LastOpenedAt is the timestamp when that software was last opened on the
// corresponding host. Only filled when the software list is requested for
// a specific host (host_id is provided).
LastOpenedAt *time.Time `json:"last_opened_at,omitempty" db:"last_opened_at"`
// TitleID is the ID of the associated software title, representing a unique combination of name
// and source.
TitleID *uint `json:"-" db:"title_id"`
// NameSource indicates whether the name for this Software was changed during the migration to
// Fleet 4.67.0
NameSource string `json:"-" db:"name_source"`
// Checksum is the unique checksum generated for this Software.
Checksum string `json:"-" db:"checksum"`
// TODO: should we create a separate type? Feels like this field shouldn't be here since it's
// just used for VPP install verification.
Installed bool `json:"-"`
// IsKernel indicates if this software is a Linux kernel.
IsKernel bool `json:"-"`
// ApplicationID is the unique identifier for Android software. Equivalent to the BundleIdentifier on Apple software.
ApplicationID *string `json:"application_id,omitempty" db:"application_id"`
DisplayName string `json:"display_name"`
}
func (Software) AuthzType() string {
return "software"
}
// populateBrowserField populates the browser field for backwards compatibility
// see https://github.com/fleetdm/fleet/pull/31760/files
func (s *Software) populateBrowserField() {
// Only populate browser field for browser extension sources
switch s.Source {
case "chrome_extensions", "firefox_addons", "ie_extensions", "safari_extensions":
s.Browser = s.ExtensionFor
default:
s.Browser = ""
}
}
// MarshalJSON populates the browser field for backwards compatibility then calls the typical
// MarshalJSON implementation
func (s *Software) MarshalJSON() ([]byte, error) {
s.populateBrowserField()
type Alias Software
return json.Marshal((*Alias)(s))
}
// ToUniqueStr creates a unique string representation of the software
func (s Software) ToUniqueStr() string {
ss := []string{s.Name, s.Version, s.Source, s.BundleIdentifier}
// Release, Vendor and Arch fields were added on a migration,
// thus we only include them in the string if at least one of them is defined.
if s.Release != "" || s.Vendor != "" || s.Arch != "" {
ss = append(ss, s.Release, s.Vendor, s.Arch)
}
// ExtensionID and ExtensionFor were added in a single migration, so they are only included if they exist.
// This way a blank ExtensionID/ExtensionFor matches the pre-migration unique string.
if s.ExtensionID != "" || s.ExtensionFor != "" {
ss = append(ss, s.ExtensionID, s.ExtensionFor)
}
if s.ApplicationID != nil && *s.ApplicationID != "" {
ss = append(ss, *s.ApplicationID)
}
return strings.Join(ss, SoftwareFieldSeparator)
}
// computeRawChecksum computes the checksum for a software entry
// The calculation must match the one in softwareChecksumComputedColumn
func (s Software) ComputeRawChecksum() ([]byte, error) {
h := md5.New() //nolint:gosec // This hash is used as a DB optimization for software row lookup, not security
cols := []string{s.Version, s.Source, s.BundleIdentifier, s.Release, s.Arch, s.Vendor, s.ExtensionFor, s.ExtensionID, s.Name}
if s.ApplicationID != nil && *s.ApplicationID != "" {
cols = append(cols, *s.ApplicationID)
}
_, err := fmt.Fprint(h, strings.Join(cols, "\x00"))
if err != nil {
return nil, err
}
return h.Sum(nil), nil
}
type VulnerableSoftware struct {
ID uint `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Version string `json:"version" db:"version"`
Source string `json:"source" db:"source"`
ExtensionFor string `json:"extension_for" db:"extension_for"`
GenerateCPE string `json:"generated_cpe" db:"generated_cpe"`
HostsCount int `json:"hosts_count,omitempty" db:"hosts_count"`
ResolvedInVersion *string `json:"resolved_in_version" db:"resolved_in_version"`
}
type VulnSoftwareFilter struct {
HostID *uint
Name string // LIKE filter
Source string // exact match
}
type SliceString []string
func (c *SliceString) Scan(v interface{}) error {
if tv, ok := v.([]byte); ok {
return json.Unmarshal(tv, &c)
}
return errors.New("unsupported type")
}
// SoftwareVersion is an abstraction over the `software` table to support the
// software titles APIs
type SoftwareVersion struct {
ID uint `db:"id" json:"id"`
// Version is the version string we grab for this specific software.
Version string `db:"version" json:"version"`
// Vulnerabilities is the list of CVE names for vulnerabilities found for this version.
Vulnerabilities *SliceString `db:"vulnerabilities" json:"vulnerabilities"`
// HostsCount is the number of hosts that use this software version.
HostsCount *uint `db:"hosts_count" json:"hosts_count,omitempty"`
// TitleID is used only as an auxiliary field and it's not part of the
// JSON response.
TitleID uint `db:"title_id" json:"-"`
}
// SoftwareTitle represents a title backed by the `software_titles` table.
type SoftwareTitle struct {
ID uint `json:"id" db:"id"`
// Name is the name reported by osquery.
Name string `json:"name" db:"name"`
// IconUrl is the URL for the software's icon, whether from VPP or via an uploaded override
IconUrl *string `json:"icon_url" db:"icon_url"`
// Source is the source reported by osquery.
Source string `json:"source" db:"source"`
// ExtensionFor is the host software that this software is an extension for
ExtensionFor string `json:"extension_for" db:"extension_for"`
// Browser is the browser type this extension is for (deprecated, use extension_for instead)
Browser string `json:"browser"`
// HostsCount is the number of hosts that use this software title.
HostsCount uint `json:"hosts_count" db:"hosts_count"`
// VesionsCount is the number of versions that have the same title.
VersionsCount uint `json:"versions_count" db:"versions_count"`
// Versions countains information about the versions that use this title.
Versions []SoftwareVersion `json:"versions" db:"-"`
// CountsUpdatedAt is the timestamp when the hosts count
// was last updated for that software title
CountsUpdatedAt *time.Time `json:"counts_updated_at" db:"counts_updated_at"`
// SoftwareInstallersCount is 0 or 1, indicating if the software title has an
// installer. This is an internal field for an optimization so that the extra
// queries to fetch installer information is done only if necessary.
SoftwareInstallersCount int `json:"-" db:"software_installers_count"`
// VPPAppsCount is 0 or 1, indicating if the software title has a VPP app.
// This is an internal field for an optimization so that the extra queries to
// fetch app information is done only if necessary.
VPPAppsCount int `json:"-" db:"vpp_apps_count"`
// InHouseAppsCount is 0 or 1, indicating if the software title has
// an in house app (.ipa) installer
InHouseAppCount int `json:"-" db:"in_house_apps_count"`
// SoftwarePackage is the software installer information for this title.
SoftwarePackage *SoftwareInstaller `json:"software_package" db:"-"`
// AppStoreApp is the VPP app information for this title.
AppStoreApp *VPPAppStoreApp `json:"app_store_app" db:"-"`
// BundleIdentifier is used by Apple installers to uniquely identify
// the software installed. It's surfaced in software_titles to match
// with existing software entries.
BundleIdentifier *string `json:"bundle_identifier,omitempty" db:"bundle_identifier"`
// IsKernel indicates if the software title is a Linux kernel.
IsKernel bool `json:"-" db:"is_kernel"`
// ApplicationID is the unique identifier for Android software. Equivalent to the BundleIdentifier on Apple software.
ApplicationID *string `json:"application_id,omitempty" db:"application_id"`
// DisplayName is an end-user friendly name.
DisplayName string `json:"display_name" db:"display_name"`
}
// populateBrowserField populates the browser field for backwards compatibility
// see https://github.com/fleetdm/fleet/pull/31760/files
func (st *SoftwareTitle) populateBrowserField() {
// Only populate browser field for browser extension sources
switch st.Source {
case "chrome_extensions", "firefox_addons", "ie_extensions", "safari_extensions":
st.Browser = st.ExtensionFor
default:
st.Browser = ""
}
}
// MarshalJSON populates the browser field for backwards compatibility then calls the typical
// MarshalJSON implementation
func (st *SoftwareTitle) MarshalJSON() ([]byte, error) {
st.populateBrowserField()
type Alias SoftwareTitle
return json.Marshal((*Alias)(st))
}
// populateBrowserField populates the browser field for backwards compatibility
// see https://github.com/fleetdm/fleet/pull/31760/files
func (st *SoftwareTitleListResult) populateBrowserField() {
// Only populate browser field for browser extension sources
switch st.Source {
case "chrome_extensions", "firefox_addons", "ie_extensions", "safari_extensions":
st.Browser = st.ExtensionFor
default:
st.Browser = ""
}
}
// MarshalJSON populates the browser field for backwards compatibility then calls the typical
// MarshalJSON implementation
func (st *SoftwareTitleListResult) MarshalJSON() ([]byte, error) {
st.populateBrowserField()
type Alias SoftwareTitleListResult
return json.Marshal((*Alias)(st))
}
// This type is essentially the same as the above SoftwareTitle type. The only difference is that
// SoftwarePackage is a string pointer here. This type is for use when listing out SoftwareTitles;
// the above type is used when fetching them individually.
type SoftwareTitleListResult struct {
ID uint `json:"id" db:"id"`
// Name is the name reported by osquery.
Name string `json:"name" db:"name"`
// IconUrl is the URL for the software's icon, whether from VPP or via an uploaded override
IconUrl *string `json:"icon_url" db:"-"`
// Source is the source reported by osquery.
Source string `json:"source" db:"source"`
// ExtensionFor is the host software that this software is an extension for
ExtensionFor string `json:"extension_for" db:"extension_for"`
// Browser is the browser type this extension is for (deprecated, use extension_for instead)
Browser string `json:"browser"`
// HostsCount is the number of hosts that use this software title.
HostsCount uint `json:"hosts_count" db:"hosts_count"`
// VesionsCount is the number of versions that have the same title.
VersionsCount uint `json:"versions_count" db:"versions_count"`
// Versions countains information about the versions that use this title.
Versions []SoftwareVersion `json:"versions" db:"-"`
// CountsUpdatedAt is the timestamp when the hosts count
// was last updated for that software title
CountsUpdatedAt *time.Time `json:"-" db:"counts_updated_at"`
// SoftwarePackage provides software installer package information, it is
// only present if a software installer is available for the software title.
SoftwarePackage *SoftwarePackageOrApp `json:"software_package"`
// AppStoreApp provides VPP app information, it is only present if a VPP app
// is available for the software title.
AppStoreApp *SoftwarePackageOrApp `json:"app_store_app"`
// BundleIdentifier is used by Apple installers to uniquely identify
// the software installed. It's surfaced in software_titles to match
// with existing software entries.
BundleIdentifier *string `json:"bundle_identifier,omitempty" db:"bundle_identifier"`
HashSHA256 *string `json:"hash_sha256,omitempty" db:"package_storage_id"`
// ApplicationID is the unique identifier for Android software. Equivalent to the BundleIdentifier on Apple software.
ApplicationID *string `json:"application_id,omitempty" db:"application_id"`
DisplayName string `json:"display_name" db:"display_name"`
}
type SoftwareTitleListOptions struct {
// ListOptions cannot be embedded in order to unmarshall with validation.
ListOptions ListOptions `url:"list_options"`
TeamID *uint `query:"team_id,optional"`
VulnerableOnly bool `query:"vulnerable,optional"`
AvailableForInstall bool `query:"available_for_install,optional"`
SelfServiceOnly bool `query:"self_service,optional"`
KnownExploit bool `query:"exploit,optional"`
MinimumCVSS float64 `query:"min_cvss_score,optional"`
MaximumCVSS float64 `query:"max_cvss_score,optional"`
PackagesOnly bool `query:"packages_only,optional"`
Platform string `query:"platform,optional"`
}
type HostSoftwareTitleListOptions struct {
// ListOptions cannot be embedded in order to unmarshal with validation.
ListOptions ListOptions `url:"list_options"`
// SelfServiceOnly limits the returned software titles to those that are
// available to install by the end user via the self-service. Implies
// AvailableForInstall.
SelfServiceOnly bool `query:"self_service,optional"`
IncludeAvailableForInstall bool `query:"include_available_for_install,optional"`
// IncludeAvailableForInstall was exposed as a query string parameter
// In order not to introduce a breaking change we have to mark this parameter as optional.
// However, instead of using *bool and modifying a lot of downstream code and tests
// Use this indicator
IncludeAvailableForInstallExplicitlySet bool
// OnlyAvailableForInstall is set via a query argument that limits the
// returned software titles to only those that are available for install on
// the host.
OnlyAvailableForInstall bool `query:"available_for_install,optional"`
VulnerableOnly bool `query:"vulnerable,optional"`
KnownExploit bool `query:"exploit,optional"`
MinimumCVSS float64 `query:"min_cvss_score,optional"`
MaximumCVSS float64 `query:"max_cvss_score,optional"`
// Non-MDM-enabled hosts cannot install VPP apps
IsMDMEnrolled bool
}
// AuthzSoftwareInventory is used for access controls on software inventory.
type AuthzSoftwareInventory struct {
// TeamID is the ID of the team. A value of nil means global scope.
TeamID *uint `json:"team_id"`
}
// AuthzType implements authz.AuthzTyper.
func (s *AuthzSoftwareInventory) AuthzType() string {
return "software_inventory"
}
type HostSoftwareEntry struct {
// Software details
Software
// Where this software was installed on the host, value is derived from the
// host_software_installed_paths table.
InstalledPaths []string `json:"installed_paths"`
PathSignatureInformation []PathSignatureInformation `json:"signature_information"`
}
// MarshalJSON implements custom JSON marshaling for HostSoftwareEntry to ensure
// all fields (both from embedded Software and the additional fields) are marshaled
func (hse *HostSoftwareEntry) MarshalJSON() ([]byte, error) {
hse.populateBrowserField()
type Alias Software
return json.Marshal(&struct {
*Alias
InstalledPaths []string `json:"installed_paths"`
PathSignatureInformation []PathSignatureInformation `json:"signature_information"`
}{
Alias: (*Alias)(&hse.Software),
InstalledPaths: hse.InstalledPaths,
PathSignatureInformation: hse.PathSignatureInformation,
})
}
type PathSignatureInformation struct {
InstalledPath string `json:"installed_path"`
TeamIdentifier string `json:"team_identifier"`
HashSha256 *string `json:"hash_sha256"`
}
// HostSoftware is the set of software installed on a specific host
type HostSoftware struct {
// Software is the software information.
Software []HostSoftwareEntry `json:"software" csv:"-"`
// SoftwareUpdatedAt is the time that the host software was last updated
SoftwareUpdatedAt time.Time `json:"software_updated_at" db:"software_updated_at" csv:"software_updated_at"`
}
type SoftwareIterator interface {
Next() bool
Value() (*Software, error)
Err() error
Close() error
}
type SoftwareListOptions struct {
// ListOptions cannot be embedded in order to unmarshall with validation.
ListOptions ListOptions `url:"list_options"`
// HostID filters software to the specified host if not nil.
HostID *uint
TeamID *uint `query:"team_id,optional"`
VulnerableOnly bool `query:"vulnerable,optional"`
WithoutVulnerabilityDetails bool `query:"without_vulnerability_details,optional"`
IncludeCVEScores bool
KnownExploit bool `query:"exploit,optional"`
MinimumCVSS float64 `query:"min_cvss_score,optional"`
MaximumCVSS float64 `query:"max_cvss_score,optional"`
// WithHostCounts indicates that the list of software should include the
// counts of hosts per software, and include only those software that have
// a count of hosts > 0.
WithHostCounts bool
}
type SoftwareIterQueryOptions struct {
ExcludedSources []string // what sources to exclude
IncludedSources []string // what sources to include
NameMatch string // mysql regex to filter software by name
NameExclude string // mysql regex to filter software by name
}
// IsValid checks that either ExcludedSources or IncludedSources is specified but not both
func (siqo SoftwareIterQueryOptions) IsValid() bool {
return !(len(siqo.IncludedSources) != 0 && len(siqo.ExcludedSources) != 0)
}
// UpdateHostSoftwareDBResult stores the 'result' of calling 'ds.UpdateHostSoftware' for a host,
// contains the software installed on the host pre-mutations all the mutations performed: what was
// inserted and what was deleted.
type UpdateHostSoftwareDBResult struct {
// What software was installed on the host before performing any mutations
WasCurrInstalled []Software
// What software was deleted
Deleted []Software
// What software was inserted
Inserted []Software
}
// CurrInstalled returns all software that should be currently installed on the host by looking at
// was currently installed, removing anything that was deleted and adding anything that was inserted
func (uhsdbr *UpdateHostSoftwareDBResult) CurrInstalled() []Software {
var r []Software
if uhsdbr == nil {
return r
}
deleteMap := map[uint]struct{}{}
for _, d := range uhsdbr.Deleted {
deleteMap[d.ID] = struct{}{}
}
for _, c := range uhsdbr.WasCurrInstalled {
if _, ok := deleteMap[c.ID]; !ok {
r = append(r, c)
}
}
r = append(r, uhsdbr.Inserted...)
return r
}
// ParseSoftwareLastOpenedAtRowValue attempts to parse the last_opened_at
// software column value. If the value is empty or if the parsed value is
// less or equal than 0 it returns (time.Time{}, nil). We do this because
// some macOS apps return "-1.0" when the app was never opened and we hardcode
// to 0 for some tables that don't have such info.
func ParseSoftwareLastOpenedAtRowValue(value string) (time.Time, error) {
if value == "" {
return time.Time{}, nil
}
lastOpenedEpoch, err := strconv.ParseFloat(value, 64)
if err != nil {
return time.Time{}, err
}
if lastOpenedEpoch <= 0 {
return time.Time{}, nil
}
return time.Unix(int64(lastOpenedEpoch), 0).UTC(), nil
}
// SoftwareFromOsqueryRow creates a fleet.Software from the values reported by osquery.
// Arguments name and source must be defined, all other fields are optional.
// This method doesn't fail if lastOpenedAt is empty or cannot be parsed.
//
// All fields are trimmed to fit on Fleet's database.
// The vendor field is currently trimmed by removing the extra characters and adding `...` at the end.
func SoftwareFromOsqueryRow(
name, version, source, vendor, installedPath, release, arch,
bundleIdentifier, extensionId, extensionFor, lastOpenedAt string,
) (*Software, error) {
if name == "" {
return nil, errors.New("host reported software with empty name")
}
if source == "" {
return nil, errors.New("host reported software with empty source")
}
// We don't fail if only the last_opened_at cannot be parsed.
lastOpenedAtTime, _ := ParseSoftwareLastOpenedAtRowValue(lastOpenedAt)
// Check whether the vendor is longer than the max allowed width and if so, truncate it.
if utf8.RuneCountInString(vendor) >= SoftwareVendorMaxLength {
vendor = fmt.Sprintf(SoftwareVendorMaxLengthFmt, vendor)
}
truncateString := func(str string, length int) string {
runes := []rune(str)
if len(runes) > length {
return string(runes[:length])
}
return str
}
software := Software{
Name: truncateString(name, SoftwareNameMaxLength),
Version: truncateString(version, SoftwareVersionMaxLength),
Source: truncateString(source, SoftwareSourceMaxLength),
BundleIdentifier: truncateString(bundleIdentifier, SoftwareBundleIdentifierMaxLength),
ExtensionID: truncateString(extensionId, SoftwareExtensionIDMaxLength),
ExtensionFor: truncateString(extensionFor, SoftwareExtensionForMaxLength),
Release: truncateString(release, SoftwareReleaseMaxLength),
Vendor: vendor,
Arch: truncateString(arch, SoftwareArchMaxLength),
}
if !lastOpenedAtTime.IsZero() {
software.LastOpenedAt = &lastOpenedAtTime
}
return &software, nil
}
type VPPBatchPayload struct {
AppStoreID string `json:"app_store_id"`
SelfService bool `json:"self_service"`
InstallDuringSetup *bool `json:"install_during_setup"` // keep saved value if nil, otherwise set as indicated
LabelsExcludeAny []string `json:"labels_exclude_any"`
LabelsIncludeAny []string `json:"labels_include_any"`
// Categories is the list of names of software categories associated with this VPP app.
Categories []string `json:"categories"`
IconPath string `json:"-"`
IconHash string `json:"-"`
}
type VPPBatchPayloadWithPlatform struct {
AppStoreID string `json:"app_store_id"`
SelfService bool `json:"self_service"`
Platform AppleDevicePlatform `json:"platform"`
InstallDuringSetup *bool `json:"install_during_setup"` // keep saved value if nil, otherwise set as indicated
LabelsExcludeAny []string `json:"labels_exclude_any"`
LabelsIncludeAny []string `json:"labels_include_any"`
// Categories is the list of names of software categories associated with this VPP app.
Categories []string `json:"categories"`
// CategoryIDs is the list of IDs of software categories associated with this VPP app.
CategoryIDs []uint `json:"-"`
}
type SoftwareCategory struct {
ID uint `db:"id"`
Name string `db:"name"`
}