diff --git a/api/models/families.go b/api/models/families.go index 9adc78bbc..6c398adb6 100644 --- a/api/models/families.go +++ b/api/models/families.go @@ -102,3 +102,15 @@ func (c *ExploitsConfig) GetScannersList() []string { return []string{"exploitdb"} } + +func (c *InfoFinderConfig) IsEnabled() bool { + return c != nil && c.Enabled != nil && *c.Enabled +} + +func (c *InfoFinderConfig) GetScannersList() []string { + if c != nil && c.Scanners != nil && len(*c.Scanners) != 0 { + return *c.Scanners + } + + return []string{"sshTopology"} +} diff --git a/api/models/models.gen.go b/api/models/models.gen.go index fb0b09881..8b5f4fcbd 100644 --- a/api/models/models.gen.go +++ b/api/models/models.gen.go @@ -31,6 +31,15 @@ const ( GCP CloudProvider = "GCP" ) +// Defines values for InfoType. +const ( + InfoTypeSSHAuthorizedKeyFingerprint InfoType = "SSHAuthorizedKeyFingerprint" + InfoTypeSSHDaemonKeyFingerprint InfoType = "SSHDaemonKeyFingerprint" + InfoTypeSSHKnownHostFingerprint InfoType = "SSHKnownHostFingerprint" + InfoTypeSSHPrivateKeyFingerprint InfoType = "SSHPrivateKeyFingerprint" + InfoTypeUNKNOWN InfoType = "UNKNOWN" +) + // Defines values for MisconfigurationSeverity. const ( MisconfigurationHighSeverity MisconfigurationSeverity = "MisconfigurationHighSeverity" @@ -55,11 +64,11 @@ const ( // Defines values for RootkitType. const ( - APPLICATION RootkitType = "APPLICATION" - FIRMWARE RootkitType = "FIRMWARE" - KERNEL RootkitType = "KERNEL" - MEMORY RootkitType = "MEMORY" - UNKNOWN RootkitType = "UNKNOWN" + RootkitTypeAPPLICATION RootkitType = "APPLICATION" + RootkitTypeFIRMWARE RootkitType = "FIRMWARE" + RootkitTypeKERNEL RootkitType = "KERNEL" + RootkitTypeMEMORY RootkitType = "MEMORY" + RootkitTypeUNKNOWN RootkitType = "UNKNOWN" ) // Defines values for ScanState. @@ -107,6 +116,7 @@ const ( // Defines values for ScanType. const ( EXPLOIT ScanType = "EXPLOIT" + INFOFINDER ScanType = "INFOFINDER" MALWARE ScanType = "MALWARE" MISCONFIGURATION ScanType = "MISCONFIGURATION" ROOTKIT ScanType = "ROOTKIT" @@ -180,6 +190,7 @@ type AssetScan struct { Exploits *ExploitScan `json:"exploits,omitempty"` FindingsProcessed *bool `json:"findingsProcessed,omitempty"` Id *string `json:"id,omitempty"` + InfoFinder *InfoFinderScan `json:"infoFinder,omitempty"` Malware *MalwareScan `json:"malware,omitempty"` Misconfigurations *MisconfigurationScan `json:"misconfigurations,omitempty"` ResourceCleanup *ResourceCleanupState `json:"resourceCleanup,omitempty"` @@ -237,6 +248,7 @@ type AssetScanRelationship struct { Exploits *ExploitScan `json:"exploits,omitempty"` FindingsProcessed *bool `json:"findingsProcessed,omitempty"` Id string `json:"id"` + InfoFinder *InfoFinderScan `json:"infoFinder,omitempty"` Malware *MalwareScan `json:"malware,omitempty"` Misconfigurations *MisconfigurationScan `json:"misconfigurations,omitempty"` ResourceCleanup *ResourceCleanupState `json:"resourceCleanup,omitempty"` @@ -283,6 +295,7 @@ type AssetScanStats struct { // General Global statistics for asset scan of all families. General *AssetScanGeneralStats `json:"general,omitempty"` + InfoFinder *[]AssetScanInputScanStats `json:"infoFinder,omitempty"` Malware *[]AssetScanInputScanStats `json:"malware,omitempty"` Misconfigurations *[]AssetScanInputScanStats `json:"misconfigurations,omitempty"` Rootkits *[]AssetScanInputScanStats `json:"rootkits,omitempty"` @@ -295,6 +308,7 @@ type AssetScanStats struct { type AssetScanStatus struct { Exploits *AssetScanState `json:"exploits,omitempty"` General *AssetScanState `json:"general,omitempty"` + InfoFinder *AssetScanState `json:"infoFinder,omitempty"` Malware *AssetScanState `json:"malware,omitempty"` Misconfigurations *AssetScanState `json:"misconfigurations,omitempty"` Rootkits *AssetScanState `json:"rootkits,omitempty"` @@ -444,6 +458,44 @@ type Findings struct { Items *[]Finding `json:"items,omitempty"` } +// InfoFinderConfig defines model for InfoFinderConfig. +type InfoFinderConfig struct { + Enabled *bool `json:"enabled,omitempty"` + Scanners *[]string `json:"scanners,omitempty"` +} + +// InfoFinderFindingInfo defines model for InfoFinderFindingInfo. +type InfoFinderFindingInfo struct { + // Data The data found by the scanner in the specific path for a specific type. See example for SSHKnownHostFingerprint info type + Data *string `json:"data,omitempty"` + ObjectType string `json:"objectType"` + + // Path File path containing the info + Path *string `json:"path,omitempty"` + ScannerName *string `json:"scannerName,omitempty"` + Type *InfoType `json:"type,omitempty"` +} + +// InfoFinderInfo defines model for InfoFinderInfo. +type InfoFinderInfo struct { + // Data The data found by the scanner in the specific path for a specific type. See example for SSHKnownHostFingerprint info type + Data *string `json:"data,omitempty"` + + // Path File path containing the info + Path *string `json:"path,omitempty"` + ScannerName *string `json:"scannerName,omitempty"` + Type *InfoType `json:"type,omitempty"` +} + +// InfoFinderScan defines model for InfoFinderScan. +type InfoFinderScan struct { + Infos *[]InfoFinderInfo `json:"infos"` + Scanners *[]string `json:"scanners"` +} + +// InfoType defines model for InfoType. +type InfoType string + // Malware defines model for Malware. type Malware struct { MalwareName *string `json:"malwareName,omitempty"` @@ -712,6 +764,7 @@ type ScanExists struct { // ScanFamiliesConfig The configuration of the scanner families within a scan config type ScanFamiliesConfig struct { Exploits *ExploitsConfig `json:"exploits,omitempty"` + InfoFinder *InfoFinderConfig `json:"infoFinder,omitempty"` Malware *MalwareConfig `json:"malware,omitempty"` Misconfigurations *MisconfigurationsConfig `json:"misconfigurations,omitempty"` Rootkits *RootkitsConfig `json:"rootkits,omitempty"` @@ -723,6 +776,7 @@ type ScanFamiliesConfig struct { // ScanFindingsSummary A summary of the scan findings. type ScanFindingsSummary struct { TotalExploits *int `json:"totalExploits,omitempty"` + TotalInfoFinder *int `json:"totalInfoFinder,omitempty"` TotalMalware *int `json:"totalMalware,omitempty"` TotalMisconfigurations *int `json:"totalMisconfigurations,omitempty"` TotalPackages *int `json:"totalPackages,omitempty"` @@ -785,6 +839,7 @@ type ScanSummary struct { JobsCompleted *int `json:"jobsCompleted,omitempty"` JobsLeftToRun *int `json:"jobsLeftToRun,omitempty"` TotalExploits *int `json:"totalExploits,omitempty"` + TotalInfoFinder *int `json:"totalInfoFinder,omitempty"` TotalMalware *int `json:"totalMalware,omitempty"` TotalMisconfigurations *int `json:"totalMisconfigurations,omitempty"` TotalPackages *int `json:"totalPackages,omitempty"` @@ -1624,6 +1679,34 @@ func (t *Finding_FindingInfo) MergeExploitFindingInfo(v ExploitFindingInfo) erro return err } +// AsInfoFinderFindingInfo returns the union data inside the Finding_FindingInfo as a InfoFinderFindingInfo +func (t Finding_FindingInfo) AsInfoFinderFindingInfo() (InfoFinderFindingInfo, error) { + var body InfoFinderFindingInfo + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromInfoFinderFindingInfo overwrites any union data inside the Finding_FindingInfo as the provided InfoFinderFindingInfo +func (t *Finding_FindingInfo) FromInfoFinderFindingInfo(v InfoFinderFindingInfo) error { + v.ObjectType = "InfoFinder" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeInfoFinderFindingInfo performs a merge with any union data inside the Finding_FindingInfo, using the provided InfoFinderFindingInfo +func (t *Finding_FindingInfo) MergeInfoFinderFindingInfo(v InfoFinderFindingInfo) error { + v.ObjectType = "InfoFinder" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JsonMerge(b, t.union) + t.union = merged + return err +} + func (t Finding_FindingInfo) Discriminator() (string, error) { var discriminator struct { Discriminator string `json:"objectType"` @@ -1640,6 +1723,8 @@ func (t Finding_FindingInfo) ValueByDiscriminator() (interface{}, error) { switch discriminator { case "Exploit": return t.AsExploitFindingInfo() + case "InfoFinder": + return t.AsInfoFinderFindingInfo() case "Malware": return t.AsMalwareFindingInfo() case "Misconfiguration": diff --git a/api/openapi.yaml b/api/openapi.yaml index c829a65fc..01a48e72e 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1169,6 +1169,8 @@ components: type: integer totalSecrets: type: integer + totalInfoFinder: + type: integer totalVulnerabilities: $ref: '#/components/schemas/VulnerabilityScanSummary' @@ -1215,6 +1217,18 @@ components: $ref: '#/components/schemas/MisconfigurationsConfig' exploits: $ref: '#/components/schemas/ExploitsConfig' + infoFinder: + $ref: '#/components/schemas/InfoFinderConfig' + + InfoFinderConfig: + type: object + properties: + enabled: + type: boolean + scanners: + type: array + items: + type: string VulnerabilitiesConfig: type: object @@ -1732,6 +1746,8 @@ components: $ref: '#/components/schemas/MisconfigurationScan' exploits: $ref: '#/components/schemas/ExploitScan' + infoFinder: + $ref: '#/components/schemas/InfoFinderScan' findingsProcessed: type: boolean resourceCleanup: @@ -1786,6 +1802,10 @@ components: type: array items: $ref: '#/components/schemas/AssetScanInputScanStats' + infoFinder: + type: array + items: + $ref: '#/components/schemas/AssetScanInputScanStats' AssetScanTemplateReadOnly: type: object @@ -1844,6 +1864,9 @@ components: exploits: $ref: '#/components/schemas/ExploitScan' readOnly: true + infoFinder: + $ref: '#/components/schemas/InfoFinderScan' + readOnly: true findingsProcessed: type: boolean readOnly: true @@ -1922,6 +1945,8 @@ components: $ref: '#/components/schemas/AssetScanState' exploits: $ref: '#/components/schemas/AssetScanState' + infoFinder: + $ref: '#/components/schemas/AssetScanState' AssetScanState: type: object @@ -2237,6 +2262,44 @@ components: $ref: '#/components/schemas/Misconfiguration' nullable: true + InfoFinderScan: + type: object + properties: + scanners: + type: array + items: + type: string + nullable: true + infos: + type: array + items: + $ref: '#/components/schemas/InfoFinderInfo' + nullable: true + + InfoFinderInfo: + type: object + properties: + scannerName: + type: string + type: + $ref: '#/components/schemas/InfoType' + path: + type: string + description: "File path containing the info" + data: + type: string + description: "The data found by the scanner in the specific path for a specific type. See example for SSHKnownHostFingerprint info type" + example: "2048 SHA256:YQuPOM8ld6FOA9HbKCgkCJWHuGt4aTRD7hstjJpRhxc xxxx (RSA)" + + InfoType: + type: string + enum: + - SSHKnownHostFingerprint + - SSHAuthorizedKeyFingerprint + - SSHPrivateKeyFingerprint + - SSHDaemonKeyFingerprint + - UNKNOWN + ExploitScan: type: object properties: @@ -2268,6 +2331,7 @@ components: - MISCONFIGURATION - ROOTKIT - EXPLOIT + - INFOFINDER FindingExists: type: object @@ -2361,6 +2425,16 @@ components: type: string required: [objectType] + InfoFinderFindingInfo: + type: object + allOf: + - $ref: '#/components/schemas/InfoFinderInfo' + - type: object + properties: + objectType: + type: string + required: [ objectType ] + Finding: type: object properties: @@ -2396,6 +2470,7 @@ components: - $ref: '#/components/schemas/MisconfigurationFindingInfo' - $ref: '#/components/schemas/RootkitFindingInfo' - $ref: '#/components/schemas/ExploitFindingInfo' + - $ref: '#/components/schemas/InfoFinderFindingInfo' discriminator: propertyName: objectType mapping: @@ -2406,6 +2481,7 @@ components: Misconfiguration: '#/components/schemas/MisconfigurationFindingInfo' Rootkit: '#/components/schemas/RootkitFindingInfo' Exploit: '#/components/schemas/ExploitFindingInfo' + InfoFinder: '#/components/schemas/InfoFinderFindingInfo' responses: Success: diff --git a/api/server/server.gen.go b/api/server/server.gen.go index 0e66958fe..25e59d179 100644 --- a/api/server/server.gen.go +++ b/api/server/server.gen.go @@ -1080,106 +1080,111 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "qXurAMsAAA==", + "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", + "ZtEAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/apiserver/database/demo.go b/pkg/apiserver/database/demo.go index 9614e259c..43ade860e 100644 --- a/pkg/apiserver/database/demo.go +++ b/pkg/apiserver/database/demo.go @@ -140,6 +140,9 @@ func createFindings(ctx context.Context, assetScans []models.AssetScan) []models if assetScan.Rootkits != nil && assetScan.Rootkits.Rootkits != nil { ret = append(ret, createRootkitFindings(ctx, findingBase, *assetScan.Rootkits.Rootkits)...) } + if assetScan.InfoFinder != nil && assetScan.InfoFinder.Infos != nil { + ret = append(ret, createInfoFinderFindings(ctx, findingBase, *assetScan.InfoFinder.Infos)...) + } } return ret @@ -295,6 +298,36 @@ func createMisconfigurationFindings(ctx context.Context, base models.Finding, mi return ret } +// nolint:gocognit,prealloc +func createInfoFinderFindings(ctx context.Context, base models.Finding, infos []models.InfoFinderInfo) []models.Finding { + logger := log.GetLoggerFromContextOrDiscard(ctx) + + var ret []models.Finding + for _, info := range infos { + val := base + convB, err := json.Marshal(info) + if err != nil { + logger.Errorf("Failed to marshal: %v", err) + continue + } + conv := models.InfoFinderFindingInfo{} + err = json.Unmarshal(convB, &conv) + if err != nil { + logger.Errorf("Failed to unmarshal: %v", err) + continue + } + val.FindingInfo = &models.Finding_FindingInfo{} + err = val.FindingInfo.FromInfoFinderFindingInfo(conv) + if err != nil { + logger.Errorf("Failed to convert FromInfoFinderFindingInfo: %v", err) + continue + } + ret = append(ret, val) + } + + return ret +} + // nolint:gocognit,prealloc func createRootkitFindings(ctx context.Context, base models.Finding, rootkits []models.Rootkit) []models.Finding { logger := log.GetLoggerFromContextOrDiscard(ctx) @@ -456,6 +489,9 @@ func createScanConfigs(_ context.Context) []models.ScanConfig { Exploits: &models.ExploitsConfig{ Enabled: utils.PointerTo(false), }, + InfoFinder: &models.InfoFinderConfig{ + Enabled: utils.PointerTo(false), + }, Malware: &models.MalwareConfig{ Enabled: utils.PointerTo(false), }, @@ -481,6 +517,9 @@ func createScanConfigs(_ context.Context) []models.ScanConfig { Exploits: &models.ExploitsConfig{ Enabled: utils.PointerTo(true), }, + InfoFinder: &models.InfoFinderConfig{ + Enabled: utils.PointerTo(true), + }, Malware: &models.MalwareConfig{ Enabled: utils.PointerTo(true), }, @@ -546,6 +585,7 @@ func createScans(assets []models.Asset, scanConfigs []models.ScanConfig) []model JobsCompleted: utils.PointerTo[int](2), JobsLeftToRun: utils.PointerTo[int](0), TotalExploits: utils.PointerTo[int](0), + TotalInfoFinder: utils.PointerTo[int](0), TotalMalware: utils.PointerTo[int](0), TotalMisconfigurations: utils.PointerTo[int](0), TotalPackages: utils.PointerTo[int](4), @@ -568,6 +608,7 @@ func createScans(assets []models.Asset, scanConfigs []models.ScanConfig) []model JobsCompleted: utils.PointerTo[int](1), JobsLeftToRun: utils.PointerTo[int](1), TotalExploits: utils.PointerTo[int](2), + TotalInfoFinder: utils.PointerTo[int](2), TotalMalware: utils.PointerTo[int](3), TotalMisconfigurations: utils.PointerTo[int](3), TotalPackages: utils.PointerTo[int](0), @@ -613,7 +654,7 @@ func createScans(assets []models.Asset, scanConfigs []models.ScanConfig) []model } } -// nolint:gocognit,maintidx +// nolint:gocognit,maintidx,cyclop func createAssetScans(scans []models.Scan) []models.AssetScan { timeNow := time.Now() @@ -878,12 +919,58 @@ func createAssetScans(scans []models.Scan) []models.AssetScan { result.Summary.TotalVulnerabilities = utils.GetVulnerabilityTotalsPerSeverity(nil) } + // Create InfoFinder if needed + if *result.ScanFamiliesConfig.InfoFinder.Enabled { + result.Status.InfoFinder = &models.AssetScanState{ + Errors: nil, + LastTransitionTime: &timeNow, + State: utils.PointerTo(models.AssetScanStateStateInProgress), + } + result.Stats.InfoFinder = &[]models.AssetScanInputScanStats{ + { + Path: utils.PointerTo("/mnt"), + ScanTime: &models.AssetScanScanTime{ + EndTime: &timeNow, + StartTime: utils.PointerTo(timeNow.Add(-5 * time.Second)), + }, + Size: utils.PointerTo(int64(300)), + Type: utils.PointerTo("rootfs"), + }, + } + result.InfoFinder = &models.InfoFinderScan{ + Infos: creatInfoFinderInfos(), + } + result.Summary.TotalInfoFinder = utils.PointerTo(len(*result.InfoFinder.Infos)) + } else { + result.Status.InfoFinder = &models.AssetScanState{ + State: utils.PointerTo(models.AssetScanStateStateNotScanned), + } + result.Summary.TotalInfoFinder = utils.PointerTo(0) + } + assetScans = append(assetScans, result) } } return assetScans } +func creatInfoFinderInfos() *[]models.InfoFinderInfo { + return &[]models.InfoFinderInfo{ + { + Data: utils.PointerTo("2048 SHA256:YQuPOM8ld6FOA9HbKCgkCJWHuGt4aTRD7hstjJpRhxc xxxx (RSA)"), + Path: utils.PointerTo("/home/ec2-user/.ssh/authorized_keys"), + ScannerName: utils.PointerTo("sshTopology"), + Type: utils.PointerTo(models.InfoTypeSSHAuthorizedKeyFingerprint), + }, + { + Data: utils.PointerTo("256 SHA256:gv6snCwAl5+6fY2g5VkmETWb9Mv0zLRkMz8aQyQWAVc xxxx (ED25519)"), + Path: utils.PointerTo("/etc/ssh/ssh_host_ed25519_key"), + ScannerName: utils.PointerTo("sshTopology"), + Type: utils.PointerTo(models.InfoTypeSSHDaemonKeyFingerprint), + }, + } +} + func createSecretsResult() *[]models.Secret { return &[]models.Secret{ { diff --git a/pkg/apiserver/database/gorm/odata.go b/pkg/apiserver/database/gorm/odata.go index 83004c187..694f5effe 100644 --- a/pkg/apiserver/database/gorm/odata.go +++ b/pkg/apiserver/database/gorm/odata.go @@ -117,6 +117,10 @@ var schemaMetas = map[string]odatasql.SchemaMeta{ FieldType: odatasql.ComplexFieldType, ComplexFieldSchemas: []string{"ExploitScan"}, }, + "infoFinder": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"InfoFinderScan"}, + }, "stats": odatasql.FieldMeta{ FieldType: odatasql.ComplexFieldType, ComplexFieldSchemas: []string{"AssetScanStats"}, @@ -163,6 +167,10 @@ var schemaMetas = map[string]odatasql.SchemaMeta{ FieldType: odatasql.ComplexFieldType, ComplexFieldSchemas: []string{"AssetScanInputScanStats"}, }, + "infoFinder": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"AssetScanInputScanStats"}, + }, }, }, "AssetScanGeneralStats": { @@ -348,6 +356,29 @@ var schemaMetas = map[string]odatasql.SchemaMeta{ "path": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, }, }, + "InfoFinderScan": { + Fields: odatasql.Schema{ + "scanners": odatasql.FieldMeta{ + FieldType: odatasql.CollectionFieldType, + CollectionItemMeta: &odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + "infos": odatasql.FieldMeta{ + FieldType: odatasql.CollectionFieldType, + CollectionItemMeta: &odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"InfoFinderInfo"}, + }, + }, + }, + }, + "InfoFinderInfo": { + Fields: odatasql.Schema{ + "scannerName": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "type": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "path": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "data": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, "ExploitScan": { Fields: odatasql.Schema{ "exploits": odatasql.FieldMeta{ @@ -431,6 +462,7 @@ var schemaMetas = map[string]odatasql.SchemaMeta{ "totalMisconfigurations": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, "totalRootkits": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, "totalSecrets": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "totalInfoFinder": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, "totalVulnerabilities": odatasql.FieldMeta{ FieldType: odatasql.ComplexFieldType, ComplexFieldSchemas: []string{"VulnerabilityScanSummary"}, @@ -539,6 +571,7 @@ var schemaMetas = map[string]odatasql.SchemaMeta{ "totalMisconfigurations": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, "totalRootkits": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, "totalSecrets": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "totalInfoFinder": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, "totalVulnerabilities": odatasql.FieldMeta{ FieldType: odatasql.ComplexFieldType, ComplexFieldSchemas: []string{"VulnerabilityScanSummary"}, @@ -614,6 +647,21 @@ var schemaMetas = map[string]odatasql.SchemaMeta{ FieldType: odatasql.ComplexFieldType, ComplexFieldSchemas: []string{"VulnerabilitiesConfig"}, }, + "infoFinder": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"InfoFinderConfig"}, + }, + }, + }, + "InfoFinderConfig": { + Fields: odatasql.Schema{ + "enabled": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "scanners": odatasql.FieldMeta{ + FieldType: odatasql.CollectionFieldType, + CollectionItemMeta: &odatasql.FieldMeta{ + FieldType: odatasql.PrimitiveFieldType, + }, + }, }, }, "ExploitsConfig": { @@ -725,6 +773,7 @@ var schemaMetas = map[string]odatasql.SchemaMeta{ "MisconfigurationFindingInfo", "RootkitFindingInfo", "ExploitFindingInfo", + "InfoFinderFindingInfo", }, DiscriminatorProperty: "objectType", DiscriminatorSchemaMapping: map[string]string{ @@ -735,6 +784,7 @@ var schemaMetas = map[string]odatasql.SchemaMeta{ "MisconfigurationFindingInfo": "Misconfiguration", "RootkitFindingInfo": "Rootkit", "ExploitFindingInfo": "Exploit", + "InfoFinderFindingInfo": "InfoFinder", }, }, }, @@ -844,6 +894,15 @@ var schemaMetas = map[string]odatasql.SchemaMeta{ "remediation": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, }, }, + "InfoFinderFindingInfo": { + Fields: odatasql.Schema{ + "objectType": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "scannerName": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "type": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "path": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + "data": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, + }, + }, "RootkitFindingInfo": { Fields: odatasql.Schema{ "objectType": odatasql.FieldMeta{FieldType: odatasql.PrimitiveFieldType}, @@ -900,6 +959,10 @@ var schemaMetas = map[string]odatasql.SchemaMeta{ FieldType: odatasql.ComplexFieldType, ComplexFieldSchemas: []string{"AssetScanState"}, }, + "infoFinder": odatasql.FieldMeta{ + FieldType: odatasql.ComplexFieldType, + ComplexFieldSchemas: []string{"AssetScanState"}, + }, }, }, "AssetScanState": { diff --git a/pkg/cli/presenter/default.go b/pkg/cli/presenter/default.go index a7cb537db..9daea0353 100644 --- a/pkg/cli/presenter/default.go +++ b/pkg/cli/presenter/default.go @@ -172,6 +172,10 @@ func (p *DefaultPresenter) ExportRootkitResult(_ context.Context, res families.F } func (p *DefaultPresenter) ExportInfoFinderResult(_ context.Context, res families.FamilyResult) error { + if res.Result == nil { + return nil + } + infoFinderResults, ok := res.Result.(*infofinder.Results) if !ok { return fmt.Errorf("failed to convert to infofinder results") diff --git a/pkg/cli/presenter/vmclarity.go b/pkg/cli/presenter/vmclarity.go index 6b58c2167..5969a2d4e 100644 --- a/pkg/cli/presenter/vmclarity.go +++ b/pkg/cli/presenter/vmclarity.go @@ -25,6 +25,7 @@ import ( "github.com/openclarity/vmclarity/pkg/shared/backendclient" "github.com/openclarity/vmclarity/pkg/shared/families" "github.com/openclarity/vmclarity/pkg/shared/families/exploits" + "github.com/openclarity/vmclarity/pkg/shared/families/infofinder" "github.com/openclarity/vmclarity/pkg/shared/families/malware" "github.com/openclarity/vmclarity/pkg/shared/families/misconfiguration" "github.com/openclarity/vmclarity/pkg/shared/families/rootkits" @@ -62,7 +63,7 @@ func (v *VMClarityPresenter) ExportFamilyResult(ctx context.Context, res familie case types.Malware: err = v.ExportMalwareResult(ctx, res) case types.InfoFinder: - err = fmt.Errorf("InfoFinder family is unsupported") + err = v.ExportInfoFinderResult(ctx, res) } return err @@ -379,6 +380,57 @@ func (v *VMClarityPresenter) ExportMisconfigurationResult(ctx context.Context, r return nil } +func (v *VMClarityPresenter) ExportInfoFinderResult(ctx context.Context, res families.FamilyResult) error { + assetScan, err := v.client.GetAssetScan(ctx, v.assetScanID, models.GetAssetScansAssetScanIDParams{}) + if err != nil { + return fmt.Errorf("failed to get asset scan: %w", err) + } + + if assetScan.Status == nil { + assetScan.Status = &models.AssetScanStatus{} + } + if assetScan.Status.InfoFinder == nil { + assetScan.Status.InfoFinder = &models.AssetScanState{} + } + if assetScan.Summary == nil { + assetScan.Summary = &models.ScanFindingsSummary{} + } + if assetScan.Stats == nil { + assetScan.Stats = &models.AssetScanStats{} + } + + var errs []string + + if res.Err != nil { + errs = append(errs, res.Err.Error()) + } else { + results, ok := res.Result.(*infofinder.Results) + if !ok { + errs = append(errs, fmt.Errorf("failed to convert to info finder results").Error()) + } else { + apiInfoFinder, err := cliutils.ConvertInfoFinderResultToAPIModel(results) + if err != nil { + errs = append(errs, fmt.Sprintf("failed to convert info finder results from scan to API model: %v", err)) + } else { + assetScan.InfoFinder = apiInfoFinder + assetScan.Summary.TotalInfoFinder = utils.PointerTo(len(results.Infos)) + } + assetScan.Stats.InfoFinder = getInputScanStats(results.Metadata.InputScans) + } + } + + state := models.AssetScanStateStateDone + assetScan.Status.InfoFinder.State = &state + assetScan.Status.InfoFinder.Errors = &errs + + err = v.client.PatchAssetScan(ctx, assetScan, v.assetScanID) + if err != nil { + return fmt.Errorf("failed to patch asset scan: %w", err) + } + + return nil +} + func (v *VMClarityPresenter) ExportRootkitResult(ctx context.Context, res families.FamilyResult) error { assetScan, err := v.client.GetAssetScan(ctx, v.assetScanID, models.GetAssetScansAssetScanIDParams{}) if err != nil { diff --git a/pkg/cli/state/vmclarity.go b/pkg/cli/state/vmclarity.go index 2b4b811fa..b259348de 100644 --- a/pkg/cli/state/vmclarity.go +++ b/pkg/cli/state/vmclarity.go @@ -173,7 +173,7 @@ func (v *VMClarityState) MarkFamilyScanInProgress(ctx context.Context, familyTyp case types.Malware: err = v.markMalwareScanInProgress(ctx) case types.InfoFinder: - err = fmt.Errorf("InfoFinder family is unsupported") + err = v.markInfoFinderScanInProgress(ctx) } return err } @@ -278,6 +278,31 @@ func (v *VMClarityState) markVulnerabilitiesScanInProgress(ctx context.Context) return nil } +func (v *VMClarityState) markInfoFinderScanInProgress(ctx context.Context) error { + assetScan, err := v.client.GetAssetScan(ctx, v.assetScanID, models.GetAssetScansAssetScanIDParams{}) + if err != nil { + return fmt.Errorf("failed to get asset scan: %w", err) + } + + if assetScan.Status == nil { + assetScan.Status = &models.AssetScanStatus{} + } + if assetScan.Status.InfoFinder == nil { + assetScan.Status.InfoFinder = &models.AssetScanState{} + } + + state := models.AssetScanStateStateInProgress + assetScan.Status.InfoFinder.State = &state + assetScan.Status.InfoFinder.LastTransitionTime = utils.PointerTo(time.Now()) + + err = v.client.PatchAssetScan(ctx, assetScan, v.assetScanID) + if err != nil { + return fmt.Errorf("failed to patch asset scan: %w", err) + } + + return nil +} + func (v *VMClarityState) markMalwareScanInProgress(ctx context.Context) error { assetScan, err := v.client.GetAssetScan(ctx, v.assetScanID, models.GetAssetScansAssetScanIDParams{}) if err != nil { diff --git a/pkg/cli/utils/apimodel.go b/pkg/cli/utils/apimodel.go index f87ef9a1b..1080ffdfc 100644 --- a/pkg/cli/utils/apimodel.go +++ b/pkg/cli/utils/apimodel.go @@ -27,6 +27,8 @@ import ( "github.com/openclarity/vmclarity/api/models" "github.com/openclarity/vmclarity/pkg/shared/families/exploits" + "github.com/openclarity/vmclarity/pkg/shared/families/infofinder" + "github.com/openclarity/vmclarity/pkg/shared/families/infofinder/types" "github.com/openclarity/vmclarity/pkg/shared/families/malware" "github.com/openclarity/vmclarity/pkg/shared/families/misconfiguration" misconfigurationTypes "github.com/openclarity/vmclarity/pkg/shared/families/misconfiguration/types" @@ -316,6 +318,46 @@ func ConvertMisconfigurationResultToAPIModel(misconfigurationResults *misconfigu }, nil } +func ConvertInfoFinderResultToAPIModel(results *infofinder.Results) (*models.InfoFinderScan, error) { + if results == nil || results.Infos == nil { + return &models.InfoFinderScan{}, nil + } + + ret := make([]models.InfoFinderInfo, len(results.Infos)) + + for i := range results.Infos { + info := results.Infos[i] + + ret[i] = models.InfoFinderInfo{ + Data: &info.Data, + Path: &info.Path, + ScannerName: &info.ScannerName, + Type: convertInfoTypeToAPIModel(info.Type), + } + } + + return &models.InfoFinderScan{ + Infos: &ret, + Scanners: utils.PointerTo(results.Metadata.Scanners), + }, nil +} + +func convertInfoTypeToAPIModel(infoType types.InfoType) *models.InfoType { + switch infoType { + case types.SSHKnownHostFingerprint: + return utils.PointerTo(models.InfoTypeSSHKnownHostFingerprint) + case types.SSHAuthorizedKeyFingerprint: + return utils.PointerTo(models.InfoTypeSSHAuthorizedKeyFingerprint) + case types.SSHPrivateKeyFingerprint: + return utils.PointerTo(models.InfoTypeSSHPrivateKeyFingerprint) + case types.SSHDaemonKeyFingerprint: + return utils.PointerTo(models.InfoTypeSSHDaemonKeyFingerprint) + default: + log.Errorf("Can't convert info type %q, treating as %v", infoType, models.InfoTypeUNKNOWN) + return utils.PointerTo(models.InfoTypeUNKNOWN) + } +} + func ConvertRootkitsResultToAPIModel(rootkitsResults *rootkits.Results) *models.RootkitScan { if rootkitsResults == nil || rootkitsResults.MergedResults == nil { return &models.RootkitScan{} @@ -339,17 +381,17 @@ func ConvertRootkitsResultToAPIModel(rootkitsResults *rootkits.Results) *models. func ConvertRootkitTypeToAPIModel(rootkitType rootkitsTypes.RootkitType) *models.RootkitType { switch rootkitType { case rootkitsTypes.APPLICATION: - return utils.PointerTo(models.APPLICATION) + return utils.PointerTo(models.RootkitTypeAPPLICATION) case rootkitsTypes.FIRMWARE: - return utils.PointerTo(models.FIRMWARE) + return utils.PointerTo(models.RootkitTypeFIRMWARE) case rootkitsTypes.KERNEL: - return utils.PointerTo(models.KERNEL) + return utils.PointerTo(models.RootkitTypeKERNEL) case rootkitsTypes.MEMORY: - return utils.PointerTo(models.MEMORY) + return utils.PointerTo(models.RootkitTypeMEMORY) case rootkitsTypes.UNKNOWN: - return utils.PointerTo(models.UNKNOWN) + return utils.PointerTo(models.RootkitTypeUNKNOWN) default: - log.Errorf("Can't convert rootkit type %q, treating as %v", rootkitType, models.UNKNOWN) - return utils.PointerTo(models.UNKNOWN) + log.Errorf("Can't convert rootkit type %q, treating as %v", rootkitType, models.RootkitTypeUNKNOWN) + return utils.PointerTo(models.RootkitTypeUNKNOWN) } } diff --git a/pkg/cli/utils/apimodel_test.go b/pkg/cli/utils/apimodel_test.go index 4a8d92bdd..8e8325a3f 100644 --- a/pkg/cli/utils/apimodel_test.go +++ b/pkg/cli/utils/apimodel_test.go @@ -29,6 +29,8 @@ import ( "github.com/openclarity/vmclarity/api/models" "github.com/openclarity/vmclarity/pkg/shared/families/exploits" common2 "github.com/openclarity/vmclarity/pkg/shared/families/exploits/common" + "github.com/openclarity/vmclarity/pkg/shared/families/infofinder" + infofinderTypes "github.com/openclarity/vmclarity/pkg/shared/families/infofinder/types" "github.com/openclarity/vmclarity/pkg/shared/families/malware" malwarecommon "github.com/openclarity/vmclarity/pkg/shared/families/malware/common" "github.com/openclarity/vmclarity/pkg/shared/families/misconfiguration" @@ -1026,3 +1028,160 @@ func Test_ConvertVulnSeverityToAPIModel(t *testing.T) { }) } } + +func TestConvertInfoFinderResultToAPIModel(t *testing.T) { + type args struct { + results *infofinder.Results + } + tests := []struct { + name string + args args + want *models.InfoFinderScan + wantErr bool + }{ + { + name: "nil results", + args: args{ + results: nil, + }, + want: &models.InfoFinderScan{}, + wantErr: false, + }, + { + name: "nil results.Info", + args: args{ + results: &infofinder.Results{ + Infos: nil, + }, + }, + want: &models.InfoFinderScan{}, + wantErr: false, + }, + { + name: "sanity", + args: args{ + results: &infofinder.Results{ + Metadata: types.Metadata{ + Scanners: []string{"scanner1", "scanner2"}, + }, + Infos: []infofinder.FlattenedInfos{ + { + ScannerName: "scanner1", + Info: infofinderTypes.Info{ + Type: infofinderTypes.SSHKnownHostFingerprint, + Path: "Path1", + Data: "Data1", + }, + }, + { + ScannerName: "scanner2", + Info: infofinderTypes.Info{ + Type: infofinderTypes.SSHDaemonKeyFingerprint, + Path: "Path2", + Data: "Data2", + }, + }, + { + ScannerName: "scanner2", + Info: infofinderTypes.Info{ + Type: infofinderTypes.SSHAuthorizedKeyFingerprint, + Path: "Path3", + Data: "Data3", + }, + }, + }, + }, + }, + want: &models.InfoFinderScan{ + Infos: utils.PointerTo([]models.InfoFinderInfo{ + { + Type: utils.PointerTo(models.InfoTypeSSHKnownHostFingerprint), + Path: utils.PointerTo("Path1"), + Data: utils.PointerTo("Data1"), + ScannerName: utils.PointerTo("scanner1"), + }, + { + Type: utils.PointerTo(models.InfoTypeSSHDaemonKeyFingerprint), + Path: utils.PointerTo("Path2"), + Data: utils.PointerTo("Data2"), + ScannerName: utils.PointerTo("scanner2"), + }, + { + Type: utils.PointerTo(models.InfoTypeSSHAuthorizedKeyFingerprint), + Path: utils.PointerTo("Path3"), + Data: utils.PointerTo("Data3"), + ScannerName: utils.PointerTo("scanner2"), + }, + }), + Scanners: utils.PointerTo([]string{"scanner1", "scanner2"}), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ConvertInfoFinderResultToAPIModel(tt.args.results) + if (err != nil) != tt.wantErr { + t.Errorf("ConvertInfoFinderResultToAPIModel() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ConvertInfoFinderResultToAPIModel() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_convertInfoTypeToAPIModel(t *testing.T) { + type args struct { + infoType infofinderTypes.InfoType + } + tests := []struct { + name string + args args + want *models.InfoType + }{ + { + name: "SSHKnownHostFingerprint", + args: args{ + infoType: infofinderTypes.SSHKnownHostFingerprint, + }, + want: utils.PointerTo(models.InfoTypeSSHKnownHostFingerprint), + }, + { + name: "SSHAuthorizedKeyFingerprint", + args: args{ + infoType: infofinderTypes.SSHAuthorizedKeyFingerprint, + }, + want: utils.PointerTo(models.InfoTypeSSHAuthorizedKeyFingerprint), + }, + { + name: "SSHPrivateKeyFingerprint", + args: args{ + infoType: infofinderTypes.SSHPrivateKeyFingerprint, + }, + want: utils.PointerTo(models.InfoTypeSSHPrivateKeyFingerprint), + }, + { + name: "SSHDaemonKeyFingerprint", + args: args{ + infoType: infofinderTypes.SSHDaemonKeyFingerprint, + }, + want: utils.PointerTo(models.InfoTypeSSHDaemonKeyFingerprint), + }, + { + name: "unknown", + args: args{ + infoType: "unknown", + }, + want: utils.PointerTo(models.InfoTypeUNKNOWN), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := convertInfoTypeToAPIModel(tt.args.infoType); !reflect.DeepEqual(got, tt.want) { + t.Errorf("convertInfoTypeToAPIModel() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/orchestrator/assetscanprocessor/infofinder.go b/pkg/orchestrator/assetscanprocessor/infofinder.go new file mode 100644 index 000000000..6b84fd151 --- /dev/null +++ b/pkg/orchestrator/assetscanprocessor/infofinder.go @@ -0,0 +1,156 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package assetscanprocessor + +import ( + "context" + "fmt" + + "github.com/openclarity/vmclarity/api/models" + "github.com/openclarity/vmclarity/pkg/shared/findingkey" + logutils "github.com/openclarity/vmclarity/pkg/shared/log" + "github.com/openclarity/vmclarity/pkg/shared/utils" +) + +func (asp *AssetScanProcessor) getExistingInfoFinderFindingsForScan(ctx context.Context, assetScan models.AssetScan) (map[findingkey.InfoFinderKey]string, error) { + logger := logutils.GetLoggerFromContextOrDiscard(ctx) + + existingMap := map[findingkey.InfoFinderKey]string{} + + existingFilter := fmt.Sprintf("findingInfo/objectType eq 'InfoFinder' and foundBy/id eq '%s'", *assetScan.Id) + existingFindings, err := asp.client.GetFindings(ctx, models.GetFindingsParams{ + Filter: &existingFilter, + Select: utils.PointerTo("id,findingInfo/scannerName,findingInfo/type,findingInfo/data,findingInfo/path"), + }) + if err != nil { + return existingMap, fmt.Errorf("failed to query for findings: %w", err) + } + + for _, finding := range *existingFindings.Items { + info, err := (*finding.FindingInfo).AsInfoFinderFindingInfo() + if err != nil { + return existingMap, fmt.Errorf("unable to get InfoFinder finding info: %w", err) + } + + key := findingkey.GenerateInfoFinderKey(info) + if _, ok := existingMap[key]; ok { + return existingMap, fmt.Errorf("found multiple matching existing findings for InfoFinder %v", key) + } + existingMap[key] = *finding.Id + } + + logger.Infof("Found %d existing InfoFinder findings for this scan", len(existingMap)) + logger.Debugf("Existing InfoFinder map: %v", existingMap) + + return existingMap, nil +} + +// nolint:cyclop +func (asp *AssetScanProcessor) reconcileResultInfoFindersToFindings(ctx context.Context, assetScan models.AssetScan) error { + completedTime := assetScan.Status.General.LastTransitionTime + + newerFound, newerTime, err := asp.newerExistingFindingTime(ctx, assetScan.Asset.Id, "InfoFinder", *completedTime) + if err != nil { + return fmt.Errorf("failed to check for newer existing InfoFinder findings: %v", err) + } + + // Build a map of existing findings for this scan to prevent us + // recreating existing ones as we might be re-reconciling the same + // asset scan because of downtime or a previous failure. + existingMap, err := asp.getExistingInfoFinderFindingsForScan(ctx, assetScan) + if err != nil { + return fmt.Errorf("failed to check existing InfoFinder findings: %w", err) + } + + if assetScan.InfoFinder != nil && assetScan.InfoFinder.Infos != nil { + // Create new or update existing findings all the infos found by the + // scan. + for _, item := range *assetScan.InfoFinder.Infos { + itemFindingInfo := models.InfoFinderFindingInfo{ + Data: item.Data, + Path: item.Path, + ScannerName: item.ScannerName, + Type: item.Type, + } + + findingInfo := models.Finding_FindingInfo{} + err = findingInfo.FromInfoFinderFindingInfo(itemFindingInfo) + if err != nil { + return fmt.Errorf("unable to convert InfoFinderFindingInfo into FindingInfo: %w", err) + } + + finding := models.Finding{ + Asset: &models.AssetRelationship{ + Id: assetScan.Asset.Id, + }, + FoundBy: &models.AssetScanRelationship{ + Id: *assetScan.Id, + }, + FoundOn: assetScan.Status.General.LastTransitionTime, + FindingInfo: &findingInfo, + } + + // Set InvalidatedOn time to the FoundOn time of the oldest + // finding, found after this asset scan. + if newerFound { + finding.InvalidatedOn = &newerTime + } + + key := findingkey.GenerateInfoFinderKey(itemFindingInfo) + if id, ok := existingMap[key]; ok { + err = asp.client.PatchFinding(ctx, id, finding) + if err != nil { + return fmt.Errorf("failed to create finding: %w", err) + } + } else { + _, err = asp.client.PostFinding(ctx, finding) + if err != nil { + return fmt.Errorf("failed to create finding: %w", err) + } + } + } + } + + // Invalidate any findings of this type for this asset where foundOn is + // older than this asset scan, and has not already been invalidated by + // an asset scan older than this asset scan. + err = asp.invalidateOlderFindingsByType(ctx, "InfoFinder", assetScan.Asset.Id, *completedTime) + if err != nil { + return fmt.Errorf("failed to invalidate older InfoFinder finding: %v", err) + } + + // Get all findings which aren't invalidated, and then update the asset's summary + asset, err := asp.client.GetAsset(ctx, assetScan.Asset.Id, models.GetAssetsAssetIDParams{}) + if err != nil { + return fmt.Errorf("failed to get asset %s: %w", assetScan.Asset.Id, err) + } + if asset.Summary == nil { + asset.Summary = &models.ScanFindingsSummary{} + } + + totalInfoFinder, err := asp.getActiveFindingsByType(ctx, "InfoFinder", assetScan.Asset.Id) + if err != nil { + return fmt.Errorf("failed to get active info finder findings: %w", err) + } + asset.Summary.TotalInfoFinder = &totalInfoFinder + + err = asp.client.PatchAsset(ctx, asset, assetScan.Asset.Id) + if err != nil { + return fmt.Errorf("failed to patch asset %s: %w", assetScan.Asset.Id, err) + } + + return nil +} diff --git a/pkg/orchestrator/assetscanprocessor/misconfigurations.go b/pkg/orchestrator/assetscanprocessor/misconfigurations.go index 101e54757..044de6b01 100644 --- a/pkg/orchestrator/assetscanprocessor/misconfigurations.go +++ b/pkg/orchestrator/assetscanprocessor/misconfigurations.go @@ -147,7 +147,7 @@ func (asp *AssetScanProcessor) reconcileResultMisconfigurationsToFindings(ctx co totalMisconfigurations, err := asp.getActiveFindingsByType(ctx, "Misconfiguration", assetScan.Asset.Id) if err != nil { - return fmt.Errorf("failed to list active critial vulnerabilities: %w", err) + return fmt.Errorf("failed to get active misconfiguration findings: %w", err) } asset.Summary.TotalMisconfigurations = &totalMisconfigurations diff --git a/pkg/orchestrator/assetscanprocessor/packages.go b/pkg/orchestrator/assetscanprocessor/packages.go index 2d8676d22..6df44fc5f 100644 --- a/pkg/orchestrator/assetscanprocessor/packages.go +++ b/pkg/orchestrator/assetscanprocessor/packages.go @@ -146,7 +146,7 @@ func (asp *AssetScanProcessor) reconcileResultPackagesToFindings(ctx context.Con totalPackages, err := asp.getActiveFindingsByType(ctx, "Package", assetScan.Asset.Id) if err != nil { - return fmt.Errorf("failed to list active critial vulnerabilities: %w", err) + return fmt.Errorf("failed to get active package findings: %w", err) } asset.Summary.TotalPackages = &totalPackages diff --git a/pkg/orchestrator/assetscanprocessor/processor.go b/pkg/orchestrator/assetscanprocessor/processor.go index dd377c1da..52cb2ddb8 100644 --- a/pkg/orchestrator/assetscanprocessor/processor.go +++ b/pkg/orchestrator/assetscanprocessor/processor.go @@ -110,6 +110,12 @@ func (asp *AssetScanProcessor) Reconcile(ctx context.Context, event AssetScanRec } } + if statusCompletedWithNoErrors(assetScan.Status.InfoFinder) { + if err := asp.reconcileResultInfoFindersToFindings(ctx, assetScan); err != nil { + return newFailedToReconcileTypeError(err, "infoFinder") + } + } + // Mark post-processing completed for this asset scan assetScan.FindingsProcessed = utils.PointerTo(true) err = asp.client.PatchAssetScan(ctx, assetScan, *assetScan.Id) diff --git a/pkg/orchestrator/assetscanprocessor/rootkits.go b/pkg/orchestrator/assetscanprocessor/rootkits.go index e300c7aea..5c6e2136f 100644 --- a/pkg/orchestrator/assetscanprocessor/rootkits.go +++ b/pkg/orchestrator/assetscanprocessor/rootkits.go @@ -142,7 +142,7 @@ func (asp *AssetScanProcessor) reconcileResultRootkitsToFindings(ctx context.Con totalRootkits, err := asp.getActiveFindingsByType(ctx, "Rootkit", assetScan.Asset.Id) if err != nil { - return fmt.Errorf("failed to list active critial vulnerabilities: %w", err) + return fmt.Errorf("failed to get active rootkit findings: %w", err) } asset.Summary.TotalRootkits = &totalRootkits diff --git a/pkg/orchestrator/assetscanprocessor/secrets.go b/pkg/orchestrator/assetscanprocessor/secrets.go index 02163ac4c..2352f2c87 100644 --- a/pkg/orchestrator/assetscanprocessor/secrets.go +++ b/pkg/orchestrator/assetscanprocessor/secrets.go @@ -146,7 +146,7 @@ func (asp *AssetScanProcessor) reconcileResultSecretsToFindings(ctx context.Cont totalSecrets, err := asp.getActiveFindingsByType(ctx, "Secret", assetScan.Asset.Id) if err != nil { - return fmt.Errorf("failed to list active critial vulnerabilities: %w", err) + return fmt.Errorf("failed to get active secret findings: %w", err) } asset.Summary.TotalSecrets = &totalSecrets diff --git a/pkg/orchestrator/assetscanwatcher/families.go b/pkg/orchestrator/assetscanwatcher/families.go index de0871885..51a3cf686 100644 --- a/pkg/orchestrator/assetscanwatcher/families.go +++ b/pkg/orchestrator/assetscanwatcher/families.go @@ -24,6 +24,7 @@ import ( "github.com/openclarity/vmclarity/pkg/shared/families/exploits" exploitsCommon "github.com/openclarity/vmclarity/pkg/shared/families/exploits/common" exploitdbConfig "github.com/openclarity/vmclarity/pkg/shared/families/exploits/exploitdb/config" + infofinderTypes "github.com/openclarity/vmclarity/pkg/shared/families/infofinder/types" "github.com/openclarity/vmclarity/pkg/shared/families/malware" clamconfig "github.com/openclarity/vmclarity/pkg/shared/families/malware/clam/config" malwarecommon "github.com/openclarity/vmclarity/pkg/shared/families/malware/common" @@ -189,6 +190,23 @@ func withMisconfigurationConfig(config *models.MisconfigurationsConfig, opts *Sc } } +func withInfoFinderConfig(config *models.InfoFinderConfig, _ *ScannerConfig) FamiliesConfigOption { + return func(c *families.Config) { + if !config.IsEnabled() { + return + } + + c.InfoFinder = infofinderTypes.Config{ + Enabled: true, + ScannersList: config.GetScannersList(), + Inputs: nil, // rootfs directory will be determined by the CLI after mount. + ScannersConfig: infofinderTypes.ScannersConfig{ + SSHTopology: infofinderTypes.SSHTopologyConfig{}, + }, + } + } +} + func withRootkitsConfig(config *models.RootkitsConfig, opts *ScannerConfig) FamiliesConfigOption { return func(c *families.Config) { if !config.IsEnabled() { @@ -219,6 +237,7 @@ func NewFamiliesConfigFrom(config *ScannerConfig, sfc *models.ScanFamiliesConfig withMalwareConfig(sfc.Malware, config), withMisconfigurationConfig(sfc.Misconfigurations, config), withRootkitsConfig(sfc.Rootkits, config), + withInfoFinderConfig(sfc.InfoFinder, config), } for _, o := range opts { diff --git a/pkg/orchestrator/scanconfigwatcher/helpers.go b/pkg/orchestrator/scanconfigwatcher/helpers.go index 0a9b2b8c6..8fac14771 100644 --- a/pkg/orchestrator/scanconfigwatcher/helpers.go +++ b/pkg/orchestrator/scanconfigwatcher/helpers.go @@ -43,6 +43,7 @@ func newScanFromScanConfig(scanConfig *models.ScanConfig) *models.Scan { TotalPackages: utils.PointerTo(0), TotalRootkits: utils.PointerTo(0), TotalSecrets: utils.PointerTo(0), + TotalInfoFinder: utils.PointerTo(0), TotalVulnerabilities: &models.VulnerabilityScanSummary{ TotalCriticalVulnerabilities: utils.PointerTo(0), TotalHighVulnerabilities: utils.PointerTo(0), diff --git a/pkg/orchestrator/scanwatcher/helpers.go b/pkg/orchestrator/scanwatcher/helpers.go index 9698bc965..2ed8238c7 100644 --- a/pkg/orchestrator/scanwatcher/helpers.go +++ b/pkg/orchestrator/scanwatcher/helpers.go @@ -41,6 +41,7 @@ func newAssetScanSummary() *models.ScanFindingsSummary { TotalPackages: utils.PointerTo[int](0), TotalRootkits: utils.PointerTo[int](0), TotalSecrets: utils.PointerTo[int](0), + TotalInfoFinder: utils.PointerTo[int](0), TotalVulnerabilities: newVulnerabilityScanSummary(), } } @@ -98,6 +99,10 @@ func newAssetScanFromScan(scan *models.Scan, assetID string) (*models.AssetScan, Errors: nil, State: getInitStateFromFamilyConfig(familiesConfig.Vulnerabilities), }, + InfoFinder: &models.AssetScanState{ + Errors: nil, + State: getInitStateFromFamilyConfig(familiesConfig.InfoFinder), + }, }, ResourceCleanup: utils.PointerTo(models.ResourceCleanupStatePending), }, nil @@ -121,6 +126,7 @@ func newScanSummary() *models.ScanSummary { TotalPackages: utils.PointerTo(0), TotalRootkits: utils.PointerTo(0), TotalSecrets: utils.PointerTo(0), + TotalInfoFinder: utils.PointerTo(0), TotalVulnerabilities: &models.VulnerabilityScanSummary{ TotalCriticalVulnerabilities: utils.PointerTo(0), TotalHighVulnerabilities: utils.PointerTo(0), @@ -156,6 +162,7 @@ func updateScanSummaryFromAssetScan(scan *models.Scan, result models.AssetScan) case models.AssetScanStateStateDone: s.JobsCompleted = utils.PointerTo(*s.JobsCompleted + 1) s.TotalExploits = utils.PointerTo(*s.TotalExploits + *r.TotalExploits) + s.TotalInfoFinder = utils.PointerTo(*s.TotalInfoFinder + *r.TotalInfoFinder) s.TotalMalware = utils.PointerTo(*s.TotalMalware + *r.TotalMalware) s.TotalMisconfigurations = utils.PointerTo(*s.TotalMisconfigurations + *r.TotalMisconfigurations) s.TotalPackages = utils.PointerTo(*s.TotalPackages + *r.TotalPackages) diff --git a/pkg/orchestrator/scanwatcher/helpers_test.go b/pkg/orchestrator/scanwatcher/helpers_test.go index 45392d338..7602c4a3b 100644 --- a/pkg/orchestrator/scanwatcher/helpers_test.go +++ b/pkg/orchestrator/scanwatcher/helpers_test.go @@ -61,6 +61,10 @@ func TestNewAssetScanFromScan(t *testing.T) { Vulnerabilities: &models.VulnerabilitiesConfig{ Enabled: utils.PointerTo(true), }, + InfoFinder: &models.InfoFinderConfig{ + Enabled: utils.PointerTo(true), + Scanners: utils.PointerTo([]string{"test"}), + }, }, }, }, @@ -104,6 +108,9 @@ func TestNewAssetScanFromScan(t *testing.T) { Errors: nil, State: utils.PointerTo(models.AssetScanStateStatePending), }, + InfoFinder: &models.AssetScanState{ + State: utils.PointerTo(models.AssetScanStateStatePending), + }, }, Summary: newAssetScanSummary(), Asset: &models.AssetRelationship{ @@ -125,6 +132,10 @@ func TestNewAssetScanFromScan(t *testing.T) { Vulnerabilities: &models.VulnerabilitiesConfig{ Enabled: utils.PointerTo(true), }, + InfoFinder: &models.InfoFinderConfig{ + Enabled: utils.PointerTo(true), + Scanners: utils.PointerTo([]string{"test"}), + }, }, }, }, diff --git a/pkg/shared/families/infofinder/sshtopology/scanner.go b/pkg/shared/families/infofinder/sshtopology/scanner.go index dbbb802e8..6a0507068 100644 --- a/pkg/shared/families/infofinder/sshtopology/scanner.go +++ b/pkg/shared/families/infofinder/sshtopology/scanner.go @@ -79,13 +79,18 @@ func (s *Scanner) Run(sourceType utils.SourceType, userInput string) error { errorsChan := make(chan error) fingerprintsChan := make(chan []types.Info) + var chanWg sync.WaitGroup + chanWg.Add(1) go func() { + defer chanWg.Done() for fingerprints := range fingerprintsChan { retResults.Infos = append(retResults.Infos, fingerprints...) } }() + chanWg.Add(1) go func() { + defer chanWg.Done() for e := range errorsChan { errs = append(errs, e) } @@ -139,6 +144,7 @@ func (s *Scanner) Run(sourceType utils.SourceType, userInput string) error { wg.Wait() close(errorsChan) close(fingerprintsChan) + chanWg.Wait() retErr := errors.Join(errs...) if retErr != nil { diff --git a/pkg/shared/findingkey/infofinder.go b/pkg/shared/findingkey/infofinder.go new file mode 100644 index 000000000..f36fd15b1 --- /dev/null +++ b/pkg/shared/findingkey/infofinder.go @@ -0,0 +1,42 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package findingkey + +import ( + "fmt" + + "github.com/openclarity/vmclarity/api/models" +) + +type InfoFinderKey struct { + ScannerName string + Type string + Data string + Path string +} + +func (k InfoFinderKey) String() string { + return fmt.Sprintf("%s.%s.%s.%s", k.ScannerName, k.Type, k.Data, k.Path) +} + +func GenerateInfoFinderKey(info models.InfoFinderFindingInfo) InfoFinderKey { + return InfoFinderKey{ + ScannerName: *info.ScannerName, + Type: string(*info.Type), + Data: *info.Data, + Path: *info.Path, + } +} diff --git a/pkg/shared/findingkey/infofinder_test.go b/pkg/shared/findingkey/infofinder_test.go new file mode 100644 index 000000000..a31095553 --- /dev/null +++ b/pkg/shared/findingkey/infofinder_test.go @@ -0,0 +1,98 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package findingkey + +import ( + "reflect" + "testing" + + "github.com/openclarity/vmclarity/api/models" + "github.com/openclarity/vmclarity/pkg/shared/utils" +) + +func TestGenerateInfoFinderKey(t *testing.T) { + type args struct { + info models.InfoFinderFindingInfo + } + tests := []struct { + name string + args args + want InfoFinderKey + }{ + { + name: "sanity", + args: args{ + info: models.InfoFinderFindingInfo{ + Data: utils.PointerTo("data"), + Path: utils.PointerTo("path"), + ScannerName: utils.PointerTo("scanner"), + Type: utils.PointerTo(models.InfoTypeSSHAuthorizedKeyFingerprint), + }, + }, + want: InfoFinderKey{ + ScannerName: "scanner", + Type: string(models.InfoTypeSSHAuthorizedKeyFingerprint), + Data: "data", + Path: "path", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GenerateInfoFinderKey(tt.args.info); !reflect.DeepEqual(got, tt.want) { + t.Errorf("GenerateInfoFinderKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestInfoFinderKey_String(t *testing.T) { + type fields struct { + ScannerName string + Type string + Data string + Path string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "sanity", + fields: fields{ + ScannerName: "scanner", + Type: string(models.InfoTypeSSHAuthorizedKeyFingerprint), + Data: "data", + Path: "path", + }, + want: "scanner.SSHAuthorizedKeyFingerprint.data.path", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := InfoFinderKey{ + ScannerName: tt.fields.ScannerName, + Type: tt.fields.Type, + Data: tt.fields.Data, + Path: tt.fields.Path, + } + if got := k.String(); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/uibackend/rest/dashboard_riskiest_assets.go b/pkg/uibackend/rest/dashboard_riskiest_assets.go index 4b0931388..971f318fe 100644 --- a/pkg/uibackend/rest/dashboard_riskiest_assets.go +++ b/pkg/uibackend/rest/dashboard_riskiest_assets.go @@ -261,7 +261,7 @@ func getCountForFindingType(summary *backendmodels.ScanFindingsSummary, findingT return summary.TotalRootkits, nil case backendmodels.SECRET: return summary.TotalSecrets, nil - case backendmodels.VULNERABILITY, backendmodels.SBOM: + case backendmodels.INFOFINDER, backendmodels.VULNERABILITY, backendmodels.SBOM: fallthrough default: return nil, fmt.Errorf("unsupported finding type: %v", findingType) @@ -282,7 +282,7 @@ func getTotalFindingFieldName(findingType backendmodels.ScanType) (string, error return totalSecretsSummaryFieldName, nil case backendmodels.VULNERABILITY: return totalVulnerabilitiesSummaryFieldName, nil - case backendmodels.SBOM: + case backendmodels.INFOFINDER, backendmodels.SBOM: fallthrough default: return "", fmt.Errorf("unsupported finding type: %v", findingType) diff --git a/pkg/uibackend/rest/dashboard_riskiest_assets_test.go b/pkg/uibackend/rest/dashboard_riskiest_assets_test.go index ac4195246..14eb882fe 100644 --- a/pkg/uibackend/rest/dashboard_riskiest_assets_test.go +++ b/pkg/uibackend/rest/dashboard_riskiest_assets_test.go @@ -198,6 +198,17 @@ func Test_getCount(t *testing.T) { want: utils.PointerTo(1), wantErr: false, }, + { + name: "TotalInfoFinder - unsupported", + args: args{ + summary: &backendmodels.ScanFindingsSummary{ + TotalInfoFinder: utils.PointerTo(1), + }, + findingType: backendmodels.INFOFINDER, + }, + want: nil, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/ui/src/layout/Scans/Configurations/ConfigurationsTable/index.js b/ui/src/layout/Scans/Configurations/ConfigurationsTable/index.js index e5cf129a1..067e6962c 100644 --- a/ui/src/layout/Scans/Configurations/ConfigurationsTable/index.js +++ b/ui/src/layout/Scans/Configurations/ConfigurationsTable/index.js @@ -23,6 +23,7 @@ const SCAN_TYPES_FILTER_ITEMS = [ "misconfigurations", "rootkits", "secrets", + "infoFinder", "sbom" ].map(type => ({value: `scanTemplate.assetScanTemplate.scanFamiliesConfig.${type}.enabled`, label: toCapitalized(type)})); @@ -73,7 +74,8 @@ const ConfigurationsTable = () => { "scanTemplate.assetScanTemplate.scanFamiliesConfig.rootkits.enabled", "scanTemplate.assetScanTemplate.scanFamiliesConfig.sbom.enabled", "scanTemplate.assetScanTemplate.scanFamiliesConfig.secrets.enabled", - "scanTemplate.assetScanTemplate.scanFamiliesConfig.vulnerabilities.enabled" + "scanTemplate.assetScanTemplate.scanFamiliesConfig.vulnerabilities.enabled", + "scanTemplate.assetScanTemplate.scanFamiliesConfig.infoFinder.enabled" ], Cell: ({row}) => { const {scanFamiliesConfig} = row.original.scanTemplate.assetScanTemplate; diff --git a/ui/src/layout/Scans/ScanConfigWizardModal/StepScanTypes.js b/ui/src/layout/Scans/ScanConfigWizardModal/StepScanTypes.js index 2fd8305cc..d40f82bc3 100644 --- a/ui/src/layout/Scans/ScanConfigWizardModal/StepScanTypes.js +++ b/ui/src/layout/Scans/ScanConfigWizardModal/StepScanTypes.js @@ -12,6 +12,7 @@ const StepScanTypes = () => { + ) } diff --git a/ui/src/layout/Scans/ScanConfigWizardModal/index.js b/ui/src/layout/Scans/ScanConfigWizardModal/index.js index 854156b86..74477d8ed 100644 --- a/ui/src/layout/Scans/ScanConfigWizardModal/index.js +++ b/ui/src/layout/Scans/ScanConfigWizardModal/index.js @@ -31,6 +31,7 @@ const ScanConfigWizardModal = ({initialData, onClose, onSubmitSuccess}) => { rootkits: {enabled: false}, secrets: {enabled: false}, misconfigurations: {enabled: false}, + infoFinder: {enabled: false}, exploits: {enabled: false} }, scanTemplate: { @@ -44,6 +45,7 @@ const ScanConfigWizardModal = ({initialData, onClose, onSubmitSuccess}) => { rootkits: {enabled: false}, secrets: {enabled: false}, misconfigurations: {enabled: false}, + infoFinder: {enabled: false}, exploits: {enabled: false} }, scannerInstanceCreationConfig: {