From fdfeb7897247f6a5fa6c7583c02c4bdc96216505 Mon Sep 17 00:00:00 2001 From: cmendible <266546+cmendible@users.noreply.github.com> Date: Fri, 7 Jun 2024 20:23:12 +0200 Subject: [PATCH] feat: aprl #246 --- .gitmodules | 3 + cmd/azqr/rules.go | 14 +- cmd/azqr/scan.go | 5 +- go.mod | 2 +- internal/aprl | 1 + internal/aprl.go | 87 +++++++++ internal/graph/graph.go | 32 +++- internal/renderers/csv/csv.go | 6 + internal/renderers/excel/excel.go | 3 +- internal/renderers/excel/impacted.go | 46 +++++ internal/renderers/excel/recommendations.go | 43 ++--- internal/renderers/excel/services.go | 2 +- internal/renderers/report_data.go | 125 +++++++++++- internal/scan.go | 202 ++++++++++++++++---- internal/scanners/adf/adf.go | 4 + internal/scanners/afd/afd.go | 4 + internal/scanners/afw/afw.go | 16 +- internal/scanners/agw/agw.go | 4 + internal/scanners/aks/aks.go | 5 + internal/scanners/amg/amg.go | 4 + internal/scanners/apim/apim.go | 16 +- internal/scanners/appcs/appcs.go | 16 +- internal/scanners/appi/appi.go | 4 + internal/scanners/as/as.go | 4 + internal/scanners/asp/asp.go | 4 + internal/scanners/ca/ca.go | 4 + internal/scanners/cae/cae.go | 4 + internal/scanners/ci/ci.go | 4 + internal/scanners/cog/cog.go | 4 + internal/scanners/cosmos/cosmos.go | 16 +- internal/scanners/cr/cr.go | 16 +- internal/scanners/dbw/dbw.go | 4 + internal/scanners/dec/dec.go | 4 + internal/scanners/evgd/evgd.go | 4 + internal/scanners/evh/evh.go | 4 + internal/scanners/kv/kv.go | 4 + internal/scanners/lb/lb.go | 4 + internal/scanners/logic/logic.go | 4 + internal/scanners/maria/maria.go | 4 + internal/scanners/mysql/mysql.go | 4 + internal/scanners/mysql/mysqlf.go | 4 + internal/scanners/psql/psql.go | 4 + internal/scanners/psql/psqlf.go | 5 + internal/scanners/redis/redis.go | 4 + internal/scanners/sb/sb.go | 4 + internal/scanners/scanner.go | 141 ++++++++++++-- internal/scanners/sigr/sigr.go | 4 + internal/scanners/sql/sql.go | 4 + internal/scanners/st/st.go | 4 + internal/scanners/synw/synw.go | 4 + internal/scanners/traf/traf.go | 16 +- internal/scanners/vgw/vgw.go | 4 + internal/scanners/vm/vm.go | 4 + internal/scanners/vmss/vmss.go | 4 + internal/scanners/vnet/vnet.go | 4 + internal/scanners/vwan/vwan.go | 4 + internal/scanners/wps/wps.go | 4 + 57 files changed, 824 insertions(+), 130 deletions(-) create mode 100644 .gitmodules create mode 160000 internal/aprl create mode 100644 internal/aprl.go create mode 100644 internal/renderers/excel/impacted.go diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..5bc12fea --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "internal/aprl"] + path = internal/aprl + url = https://github.com/Azure/Azure-Proactive-Resiliency-Library-v2.git diff --git a/cmd/azqr/rules.go b/cmd/azqr/rules.go index b9ea07f2..09794b6e 100644 --- a/cmd/azqr/rules.go +++ b/cmd/azqr/rules.go @@ -6,6 +6,7 @@ package azqr import ( "fmt" "sort" + "strings" "github.com/Azure/azqr/internal" "github.com/Azure/azqr/internal/scanners" @@ -23,9 +24,10 @@ var rulesCmd = &cobra.Command{ Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { serviceScanners := internal.GetScanners() + aprl := internal.GetAprlRecommendations() - fmt.Println("# | Id | Category | Impact | Recommendation | More Info") - fmt.Println("---|---|---|---|---|---") + fmt.Println("# | Resource Type | Id | Category | Impact | Recommendation | Learn") + fmt.Println("---|---|---|---|---|---|---") i := 0 for _, scanner := range serviceScanners { @@ -45,7 +47,13 @@ var rulesCmd = &cobra.Command{ for _, k := range keys { rule := rules[k] i++ - fmt.Printf("%s | %s | %s | %s | %s | [Learn](%s)", fmt.Sprint(i), rule.Id, rule.Category, rule.Impact, rule.Recommendation, rule.Url) + fmt.Printf("%s | %s | %s | %s | %s | %s | [Learn](%s)", fmt.Sprint(i), rule.Id, scanner.ResourceType(), rule.Category, rule.Impact, rule.Recommendation, rule.Url) + fmt.Println() + } + + for _, r := range aprl[strings.ToLower(scanner.ResourceType())] { + i++ + fmt.Printf("%s | %s | %s | %s | %s | %s | [Learn](%s)", fmt.Sprint(i), r.RecommendationID, scanner.ResourceType(), r.Category, r.Impact, r.Recommendation, r.LearnMoreLink[0].Url) fmt.Println() } } diff --git a/cmd/azqr/scan.go b/cmd/azqr/scan.go index ee98bd9a..e183bf6b 100644 --- a/cmd/azqr/scan.go +++ b/cmd/azqr/scan.go @@ -16,12 +16,13 @@ func init() { scanCmd.PersistentFlags().BoolP("defender", "d", true, "Scan Defender Status") scanCmd.PersistentFlags().BoolP("advisor", "a", true, "Scan Azure Advisor Recommendations") scanCmd.PersistentFlags().BoolP("costs", "c", false, "Scan Azure Costs") - scanCmd.PersistentFlags().BoolP("excel", "x", false, "Create excel report") + scanCmd.PersistentFlags().BoolP("excel", "x", true, "Create excel report") scanCmd.PersistentFlags().StringP("output-name", "o", "", "Output file name without extension") scanCmd.PersistentFlags().BoolP("mask", "m", true, "Mask the subscription id in the report") scanCmd.PersistentFlags().BoolP("azure-cli-credential", "f", false, "Force the use of Azure CLI Credential") scanCmd.PersistentFlags().BoolP("debug", "", false, "Set log level to debug") scanCmd.PersistentFlags().StringP("exclusions", "e", "", "Exclusions file (YAML format)") + scanCmd.PersistentFlags().BoolP("azqr", "", true, "Scan Azure Quick Review Recommendations (default)") rootCmd.AddCommand(scanCmd) } @@ -49,6 +50,7 @@ func scan(cmd *cobra.Command, serviceScanners []scanners.IAzureScanner) { debug, _ := cmd.Flags().GetBool("debug") forceAzureCliCredential, _ := cmd.Flags().GetBool("azure-cli-credential") exclusionFile, _ := cmd.Flags().GetString("exclusions") + azqr, _ := cmd.Flags().GetBool("azqr") params := internal.ScanParams{ SubscriptionID: subscriptionID, @@ -63,6 +65,7 @@ func scan(cmd *cobra.Command, serviceScanners []scanners.IAzureScanner) { ServiceScanners: serviceScanners, ForceAzureCliCredential: forceAzureCliCredential, ExclusionsFile: exclusionFile, + UseAzqrRecommendations: azqr, } internal.Scan(¶ms) diff --git a/go.mod b/go.mod index ddb432fa..ce71391e 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/synapse/armsynapse v0.8.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/trafficmanager/armtrafficmanager v1.3.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/webpubsub/armwebpubsub v1.2.0 + github.com/google/uuid v1.6.0 github.com/rs/zerolog v1.32.0 github.com/spf13/cobra v1.8.0 github.com/xuri/excelize/v2 v2.8.1 @@ -57,7 +58,6 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.6.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/internal/aprl b/internal/aprl new file mode 160000 index 00000000..42e88edb --- /dev/null +++ b/internal/aprl @@ -0,0 +1 @@ +Subproject commit 42e88edb05e14e0ccc465aaad5df2ad6eb7bf511 diff --git a/internal/aprl.go b/internal/aprl.go new file mode 100644 index 00000000..6da73c1f --- /dev/null +++ b/internal/aprl.go @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package internal + +import ( + "embed" + "io/fs" + "strings" + + "github.com/Azure/azqr/internal/scanners" + "gopkg.in/yaml.v3" +) + +//go:embed aprl/azure-resources/**/**/*.yaml +//go:embed aprl/azure-resources/**/**/kql/*.kql +//go:embed aprl/azure-specialized-workloads/**/*.yaml +//go:embed aprl/azure-specialized-workloads/**/kql/*.kql +var embededFiles embed.FS + +// GetAprlRecommendations +func GetAprlRecommendations() map[string]map[string]scanners.AzureAprlRecommendation { + r := map[string]map[string]scanners.AzureAprlRecommendation{} + + fsys, err := fs.Sub(embededFiles, "aprl/azure-resources") + if err != nil { + return nil + } + + q := map[string]string{} + err = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() && strings.HasSuffix(path, ".kql") { + content, err := fs.ReadFile(fsys, path) + if err != nil { + return err + } + + fileName := strings.TrimSuffix(d.Name(), ".kql") + q[fileName] = string(content) + } + return nil + }) + if err != nil { + return nil + } + + err = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() && strings.HasSuffix(path, ".yaml") { + content, err := fs.ReadFile(fsys, path) + if err != nil { + return err + } + + var recommendations []scanners.AzureAprlRecommendation + err = yaml.Unmarshal(content, &recommendations) + if err != nil { + return err + } + + for _, recommendation := range recommendations { + t := strings.ToLower(recommendation.ResourceType) + if _, ok := r[t]; !ok { + r[t] = map[string]scanners.AzureAprlRecommendation{} + } + + if i, ok := q[recommendation.RecommendationID]; ok { + recommendation.GraphQuery = i + } + + r[t][recommendation.RecommendationID] = recommendation + } + + } + return nil + }) + if err != nil { + return nil + } + + return r +} diff --git a/internal/graph/graph.go b/internal/graph/graph.go index 73081fc9..838b385f 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -5,6 +5,7 @@ package graph import ( "context" + "time" "github.com/Azure/azqr/internal/to" "github.com/Azure/azure-sdk-for-go/sdk/azcore" @@ -55,15 +56,42 @@ func (q *GraphQuery) Query(ctx context.Context, query string, subscriptionIDs [] for ok := true; ok; ok = skipToken != nil { request.Options.SkipToken = skipToken // Run the query and get the results - results, err := q.client.Resources(ctx, request, nil) + results, err := q.retry(ctx, 3, 10*time.Second, request) if err == nil { result.Count = *results.TotalRecords result.Data = append(result.Data, results.Data.([]interface{})...) skipToken = results.SkipToken } else { - log.Fatal().Err(err).Msg("Failed to run Resource Graph query") + log.Fatal().Err(err).Msgf("Failed to run Resource Graph query: %s", query) return nil } } return &result } + +func (q *GraphQuery) retry(ctx context.Context, attempts int, sleep time.Duration, request arg.QueryRequest) (arg.ClientResourcesResponse, error) { + var err error + for i := 0; ; i++ { + res, err := q.client.Resources(ctx, request, nil) + if err == nil { + return res, nil + } + + // if shouldSkipError(err) { + // return []scanners.AzureServiceResult{}, nil + // } + + errAsString := err.Error() + + if i >= (attempts - 1) { + log.Info().Msgf("Retry limit reached. Error: %s", errAsString) + break + } + + log.Debug().Msgf("Retrying after error: %s", errAsString) + + time.Sleep(sleep) + sleep *= 2 + } + return arg.ClientResourcesResponse{}, err +} diff --git a/internal/renderers/csv/csv.go b/internal/renderers/csv/csv.go index 6cb5f1c6..a8575639 100644 --- a/internal/renderers/csv/csv.go +++ b/internal/renderers/csv/csv.go @@ -17,6 +17,12 @@ func CreateCsvReport(data *renderers.ReportData) { records := data.ServicesTable() writeData(records, data.OutputFileName, "services") + records = data.RecommendationsTable() + writeData(records, data.OutputFileName, "recommendations") + + records = data.ImpactedTable() + writeData(records, data.OutputFileName, "impacted") + records = data.DefenderTable() writeData(records, data.OutputFileName, "defender") diff --git a/internal/renderers/excel/excel.go b/internal/renderers/excel/excel.go index 2414e181..b71f8a55 100644 --- a/internal/renderers/excel/excel.go +++ b/internal/renderers/excel/excel.go @@ -8,8 +8,8 @@ import ( _ "image/png" "unicode/utf8" - "github.com/Azure/azqr/internal/renderers" "github.com/Azure/azqr/internal/embeded" + "github.com/Azure/azqr/internal/renderers" "github.com/rs/zerolog/log" "github.com/xuri/excelize/v2" ) @@ -25,6 +25,7 @@ func CreateExcelReport(data *renderers.ReportData) { }() renderRecommendations(f, data) + renderImpactedResources(f, data) renderServices(f, data) renderDefender(f, data) renderAdvisor(f, data) diff --git a/internal/renderers/excel/impacted.go b/internal/renderers/excel/impacted.go new file mode 100644 index 00000000..bf65eb17 --- /dev/null +++ b/internal/renderers/excel/impacted.go @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package excel + +import ( + _ "image/png" + + "github.com/Azure/azqr/internal/renderers" + "github.com/rs/zerolog/log" + "github.com/xuri/excelize/v2" +) + +func renderImpactedResources(f *excelize.File, data *renderers.ReportData) { + sheetName := "Impacted Resources" + if len(data.AprlData) > 0 { + _, err := f.NewSheet(sheetName) + if err != nil { + log.Fatal().Err(err).Msg("Failed to create APRL sheet") + } + + records := data.ImpactedTable() + headers := records[0] + records = records[1:] + + createFirstRow(f, sheetName, headers) + + currentRow := 4 + for _, row := range records { + currentRow += 1 + cell, err := excelize.CoordinatesToCellName(1, currentRow) + if err != nil { + log.Fatal().Err(err).Msg("Failed to get cell") + } + err = f.SetSheetRow(sheetName, cell, &row) + if err != nil { + log.Fatal().Err(err).Msg("Failed to set row") + } + setHyperLink(f, sheetName, 12, currentRow) + } + + configureSheet(f, sheetName, headers, currentRow) + } else { + log.Info().Msgf("Skipping %s. No data to render", sheetName) + } +} diff --git a/internal/renderers/excel/recommendations.go b/internal/renderers/excel/recommendations.go index 8d285444..e0fbb5fd 100644 --- a/internal/renderers/excel/recommendations.go +++ b/internal/renderers/excel/recommendations.go @@ -12,52 +12,35 @@ import ( ) func renderRecommendations(f *excelize.File, data *renderers.ReportData) { - if len(data.MainData) > 0 { - err := f.SetSheetName("Sheet1", "Recommendations") + sheetName := "Recommendations" + if len(data.Recomendations) > 0 { + err := f.SetSheetName("Sheet1", sheetName) if err != nil { - log.Fatal().Err(err).Msg("Failed to create Recommendations sheet") + log.Fatal().Err(err).Msgf("Failed to create %s sheet", sheetName) } - renderedRules := map[string]bool{} - - headers := []string{"Category", "Impact", "Recommendation", "Learn", "RId"} - rows := [][]string{} - for _, result := range data.MainData { - for _, rr := range result.Rules { - _, exists := renderedRules[rr.Id] - if !exists && rr.NotCompliant { - rulesToRender := map[string]string{ - "Category": string(rr.Category), - "Impact": string(rr.Impact), - "Recommendation": rr.Recommendation, - "Learn": rr.Learn, - "RId": rr.Id, - } - renderedRules[rr.Id] = true - rows = append(rows, mapToRow(headers, rulesToRender)...) - } - } - } + records := data.RecommendationsTable() + headers := records[0] + records = records[1:] - createFirstRow(f, "Recommendations", headers) + createFirstRow(f, sheetName, headers) currentRow := 4 - for _, row := range rows { + for _, row := range records { currentRow += 1 cell, err := excelize.CoordinatesToCellName(1, currentRow) if err != nil { log.Fatal().Err(err).Msg("Failed to get cell") } - err = f.SetSheetRow("Recommendations", cell, &row) + err = f.SetSheetRow(sheetName, cell, &row) if err != nil { log.Fatal().Err(err).Msg("Failed to set row") } - - setHyperLink(f, "Recommendations", 6, currentRow) + // setHyperLink(f, sheetName, 12, currentRow) } - configureSheet(f, "Recommendations", headers, currentRow) + configureSheet(f, sheetName, headers, currentRow) } else { - log.Info().Msg("Skipping Recommendations. No data to render") + log.Info().Msgf("Skipping %s. No data to render", sheetName) } } diff --git a/internal/renderers/excel/services.go b/internal/renderers/excel/services.go index 08ab225d..dbbd9050 100644 --- a/internal/renderers/excel/services.go +++ b/internal/renderers/excel/services.go @@ -12,7 +12,7 @@ import ( ) func renderServices(f *excelize.File, data *renderers.ReportData) { - if len(data.MainData) > 0 { + if len(data.AzqrData) > 0 { _, err := f.NewSheet("Services") if err != nil { log.Fatal().Err(err).Msg("Failed to create Services sheet") diff --git a/internal/renderers/report_data.go b/internal/renderers/report_data.go index 644e0981..c48872fb 100644 --- a/internal/renderers/report_data.go +++ b/internal/renderers/report_data.go @@ -5,17 +5,21 @@ package renderers import ( "fmt" + "strings" "github.com/Azure/azqr/internal/scanners" + "github.com/google/uuid" ) type ReportData struct { OutputFileName string Mask bool - MainData []scanners.AzureServiceResult + AzqrData []scanners.AzureServiceResult + AprlData []scanners.AzureServiceGraphRuleResult DefenderData []scanners.DefenderResult AdvisorData []scanners.AdvisorResult CostData *scanners.CostResult + Recomendations map[string]map[string]scanners.AzureAprlRecommendation } func (rd *ReportData) ServicesTable() [][]string { @@ -23,7 +27,7 @@ func (rd *ReportData) ServicesTable() [][]string { rbroken := [][]string{} rok := [][]string{} - for _, d := range rd.MainData { + for _, d := range rd.AzqrData { for _, r := range d.Rules { row := []string{ scanners.MaskSubscriptionID(d.SubscriptionID, rd.Mask), @@ -38,7 +42,7 @@ func (rd *ReportData) ServicesTable() [][]string { r.Recommendation, r.Result, r.Learn, - r.Id, + r.RecommendationID, } if r.NotCompliant { rbroken = append([][]string{row}, rbroken...) @@ -53,6 +57,66 @@ func (rd *ReportData) ServicesTable() [][]string { return rows } +func (rd *ReportData) ImpactedTable() [][]string { + headers := []string{"Validated Using", "Source", "Category", "Impact", "Resource Type", "Recommendation", "Recommendation Id", "Subscripyion Id", "Subscription Name", "Resource Group", "Name", "Id", "Param1", "Param2", "Param3", "Param4", "Param5", "Learn"} + + rows := [][]string{} + for _, r := range rd.AprlData { + row := []string{ + "Azure Resource Graph", + r.Source, + string(r.Category), + string(r.Impact), + r.ResourceType, + r.Recommendation, + r.RecommendationID, + scanners.MaskSubscriptionID(r.SubscriptionID, rd.Mask), + r.SubscriptionName, + r.ResourceGroup, + r.Name, + scanners.MaskSubscriptionIDInResourceID(r.ResourceID, rd.Mask), + r.Param1, + r.Param2, + r.Param3, + r.Param4, + r.Param5, + r.Learn, + } + rows = append(rows, row) + } + + for _, d := range rd.AzqrData { + for _, r := range d.Rules { + if r.NotCompliant { + row := []string{ + "Golang SDK", + "AZQR", + string(r.Category), + string(r.Impact), + d.Type, + r.Recommendation, + r.RecommendationID, + scanners.MaskSubscriptionID(d.SubscriptionID, rd.Mask), + d.SubscriptionName, + d.ResourceGroup, + d.ServiceName, + scanners.MaskSubscriptionIDInResourceID(d.ResourceID(), rd.Mask), + r.Result, + "", + "", + "", + "", + r.Learn, + } + rows = append(rows, row) + } + } + } + + rows = append([][]string{headers}, rows...) + return rows +} + func (rd *ReportData) CostTable() [][]string { headers := []string{"From", "To", "Subscription", "Subscription Name", "ServiceName", "Value", "Currency"} @@ -113,3 +177,58 @@ func (rd *ReportData) AdvisorTable() [][]string { rows = append([][]string{headers}, rows...) return rows } + +func (rd *ReportData) RecommendationsTable() [][]string { + counter := map[string]int{} + for _, rt := range rd.Recomendations { + for _, r := range rt { + counter[r.RecommendationID] = 0 + } + } + + for _, r := range rd.AprlData { + counter[r.RecommendationID]++ + } + + for _, d := range rd.AzqrData { + for _, r := range d.Rules { + if r.NotCompliant { + counter[r.RecommendationID]++ + } + } + } + + headers := []string{"Implemented", "Number of Impacted Resources", "Azure Service / Well-Architected", "Recommendation Source", + "Azure Service Category / Well-Architected Area", "Azure Service / Well-Architected Topic", "Resiliency Category", "Recommendation", + "Impact", "Best Practices Guidance", "Read More", "Recommendation Id"} + rows := [][]string{} + for t, rt := range rd.Recomendations { + for _, r := range rt { + implemented := counter[r.RecommendationID] == 0 + source := "APRL" + _, err := uuid.Parse(r.RecommendationID) + if err != nil { + source = "AZQR" + } + + row := []string{ + fmt.Sprintf("%t", implemented), + fmt.Sprint(counter[r.RecommendationID]), + "Azure Service", + source, + strings.Split(t, "/")[0], + strings.Split(t, "/")[1], + string(r.Category), + r.Recommendation, + string(r.Impact), + r.LongDescription, + r.LearnMoreLink[0].Url, + r.RecommendationID, + } + rows = append(rows, row) + } + } + + rows = append([][]string{headers}, rows...) + return rows +} diff --git a/internal/scan.go b/internal/scan.go index ff936678..0ed4f16f 100644 --- a/internal/scan.go +++ b/internal/scan.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/Azure/azqr/internal/graph" "github.com/Azure/azqr/internal/renderers" "github.com/Azure/azqr/internal/renderers/csv" "github.com/Azure/azqr/internal/renderers/excel" @@ -82,6 +83,8 @@ type ScanParams struct { ServiceScanners []scanners.IAzureScanner ForceAzureCliCredential bool ExclusionsFile string + UseAzqrRecommendations bool + UseAprlRecommendations bool } func Scan(params *ScanParams) { @@ -156,6 +159,8 @@ func Scan(params *ScanParams) { ctx := context.Background() + graph := graph.NewGraphQuery(cred) + clientOptions := &arm.ClientOptions{ ClientOptions: policy.ClientOptions{ Retry: policy.RetryOptions{ @@ -178,6 +183,7 @@ func Scan(params *ScanParams) { } var ruleResults []scanners.AzureServiceResult + var graphRuleResults []scanners.AzureServiceGraphRuleResult var defenderResults []scanners.DefenderResult var advisorResults []scanners.AdvisorResult costResult := &scanners.CostResult{ @@ -194,15 +200,18 @@ func Scan(params *ScanParams) { advisorScanner := scanners.AdvisorScanner{} costScanner := scanners.CostScanner{} - for s, sn := range subscriptions { - if exclusions.Azqr.Exclude.IsSubscriptionExcluded(s) { - log.Info().Msgf("Skipping subscriptions/...%s", s[29:]) + aprl := GetAprlRecommendations() + usedAprl := map[string]map[string]scanners.AzureAprlRecommendation{} + + for sid, sn := range subscriptions { + if exclusions.Azqr.Exclude.IsSubscriptionExcluded(sid) { + log.Info().Msgf("Skipping subscriptions/...%s", sid[29:]) continue } resourceGroups := []string{} if resourceGroupName != "" { - exists, err := checkExistenceResourceGroup(ctx, s, resourceGroupName, cred, clientOptions) + exists, err := checkExistenceResourceGroup(ctx, sid, resourceGroupName, cred, clientOptions) if err != nil { log.Fatal().Err(err).Msg("Failed to check existence of Resource Group") } @@ -211,20 +220,20 @@ func Scan(params *ScanParams) { log.Fatal().Msgf("Resource Group %s does not exist", resourceGroupName) } - if exclusions.Azqr.Exclude.IsResourceGroupExcluded(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", s, resourceGroupName)) { - log.Info().Msgf("Skipping subscriptions/...%s/resourceGroups/%s", s[29:], resourceGroupName) + if exclusions.Azqr.Exclude.IsResourceGroupExcluded(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", sid, resourceGroupName)) { + log.Info().Msgf("Skipping subscriptions/...%s/resourceGroups/%s", sid[29:], resourceGroupName) continue } resourceGroups = append(resourceGroups, resourceGroupName) } else { - rgs, err := listResourceGroup(ctx, s, cred, clientOptions) + rgs, err := listResourceGroup(ctx, sid, cred, clientOptions) if err != nil { log.Fatal().Err(err).Msg("Failed to list Resource Groups") } for _, rg := range rgs { - if exclusions.Azqr.Exclude.IsResourceGroupExcluded(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", s, *rg.Name)) { - log.Info().Msgf("Skipping subscriptions/...%s/resourceGroups/%s", s[29:], *rg.Name) + if exclusions.Azqr.Exclude.IsResourceGroupExcluded(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", sid, *rg.Name)) { + log.Info().Msgf("Skipping subscriptions/...%s/resourceGroups/%s", sid[29:], *rg.Name) continue } resourceGroups = append(resourceGroups, *rg.Name) @@ -233,7 +242,7 @@ func Scan(params *ScanParams) { config := &scanners.ScannerConfig{ Ctx: ctx, - SubscriptionID: s, + SubscriptionID: sid, SubscriptionName: sn, Cred: cred, ClientOptions: clientOptions, @@ -285,43 +294,88 @@ func Scan(params *ScanParams) { PublicIPs: pips, } + var wg sync.WaitGroup + ch := make(chan []scanners.AzureServiceGraphRuleResult, 5) + wg.Add(len(params.ServiceScanners)) + + go func() { + wg.Wait() + close(ch) + }() + for _, a := range params.ServiceScanners { err := a.Init(config) if err != nil { log.Fatal().Err(err).Msg("Failed to initialize scanner") } - } - for _, r := range resourceGroups { - var wg sync.WaitGroup - ch := make(chan []scanners.AzureServiceResult, 5) - wg.Add(len(params.ServiceScanners)) + usedAprl[a.ResourceType()] = map[string]scanners.AzureAprlRecommendation{} - go func() { - wg.Wait() - close(ch) - }() + if params.UseAzqrRecommendations { + for i, r := range a.GetRules() { + usedAprl[a.ResourceType()][i] = r.ToAzureAprlRecommendation(a.ResourceType()) + } + } - for _, s := range params.ServiceScanners { - go func(r string, s scanners.IAzureScanner) { - defer wg.Done() + go func(s scanners.IAzureScanner) { + scanners.LogSubscriptionScan(sid, s.ResourceType()) + rules := scanners.GetGraphRules(s.ResourceType(), aprl) - res, err := retry(3, 10*time.Millisecond, s, r, &scanContext) - if err != nil { - cancel() - log.Fatal().Err(err).Msg("Failed to scan") - } - ch <- res - }(r, s) + for i, r := range rules { + usedAprl[s.ResourceType()][i] = r + } + + res, err := graphScan(graph, rules, config) + if err != nil { + cancel() + log.Fatal().Err(err).Msg("Failed to scan") + } + ch <- res + }(a) + } + + for i := 0; i < len(params.ServiceScanners); i++ { + res := <-ch + for _, r := range res { + if exclusions.Azqr.Exclude.IsServiceExcluded(r.ResourceID) { + continue + } + graphRuleResults = append(graphRuleResults, r) } + } + + if params.UseAzqrRecommendations { + for _, r := range resourceGroups { + var wg sync.WaitGroup + ch := make(chan []scanners.AzureServiceResult, 5) + wg.Add(len(params.ServiceScanners)) + + go func() { + wg.Wait() + close(ch) + }() + + for _, s := range params.ServiceScanners { + go func(r string, s scanners.IAzureScanner) { + defer wg.Done() + + res, err := retry(3, 10*time.Millisecond, s, r, &scanContext) + if err != nil { + cancel() + log.Fatal().Err(err).Msg("Failed to scan") + } + ch <- res + }(r, s) + } - for i := 0; i < len(params.ServiceScanners); i++ { - res := <-ch - for _, r := range res { - if exclusions.Azqr.Exclude.IsServiceExcluded(r.ResourceID()) { - continue + for i := 0; i < len(params.ServiceScanners); i++ { + res := <-ch + for _, r := range res { + if exclusions.Azqr.Exclude.IsServiceExcluded(r.ResourceID()) { + continue + } + ruleResults = append(ruleResults, r) } - ruleResults = append(ruleResults, r) } } } @@ -378,7 +432,9 @@ func Scan(params *ScanParams) { reportData := renderers.ReportData{ OutputFileName: outputFile, Mask: mask, - MainData: ruleResults, + Recomendations: usedAprl, + AzqrData: ruleResults, + AprlData: graphRuleResults, DefenderData: defenderResults, AdvisorData: advisorResults, CostData: costResult, @@ -489,6 +545,80 @@ func shouldSkipError(err error) bool { return false } +func graphScan(graphClient *graph.GraphQuery, rules map[string]scanners.AzureAprlRecommendation, config *scanners.ScannerConfig) ([]scanners.AzureServiceGraphRuleResult, error) { + // scanners.LogResourceGroupScan(a.config.SubscriptionID, resourceGroupName, "AKS") + results := []scanners.AzureServiceGraphRuleResult{} + + for _, rule := range rules { + if rule.GraphQuery != "" { + result := graphClient.Query(config.Ctx, rule.GraphQuery, []*string{&config.SubscriptionID}) + if result.Data != nil { + for _, row := range result.Data { + m := row.(map[string]interface{}) + + tags := "" + // if m["tags"] != nil { + // tags = m["tags"].(string) + // } + + param1 := "" + if m["param1"] != nil { + param1 = m["param1"].(string) + } + + param2 := "" + if m["param2"] != nil { + param2 = m["param2"].(string) + } + + param3 := "" + if m["param3"] != nil { + param3 = m["param3"].(string) + } + + param4 := "" + if m["param4"] != nil { + param4 = m["param4"].(string) + } + + param5 := "" + if m["param5"] != nil { + param5 = m["param5"].(string) + } + + log.Debug().Msg(rule.GraphQuery) + + results = append(results, scanners.AzureServiceGraphRuleResult{ + RecommendationID: rule.RecommendationID, + Category: scanners.RulesCategory(rule.Category), + Recommendation: rule.Recommendation, + ResourceType: rule.ResourceType, + LongDescription: rule.LongDescription, + PotentialBenefits: rule.PotentialBenefits, + Impact: scanners.ImpactType(rule.Impact), + Name: m["name"].(string), + ResourceID: m["id"].(string), + SubscriptionID: scanners.GetSubsctiptionFromResourceID(m["id"].(string)), + SubscriptionName: config.SubscriptionName, + ResourceGroup: scanners.GetResourceGroupFromResourceID(m["id"].(string)), + Tags: tags, + Param1: param1, + Param2: param2, + Param3: param3, + Param4: param4, + Param5: param5, + Learn: rule.LearnMoreLink[0].Url, + AutomationAvailable: rule.AutomationAvailable, + Source: "APRL", + }) + } + } + } + } + + return results, nil +} + func GetScanners() []scanners.IAzureScanner { return []scanners.IAzureScanner{ &dbw.DatabricksScanner{}, diff --git a/internal/scanners/adf/adf.go b/internal/scanners/adf/adf.go index d20afa9d..c7da3dc3 100644 --- a/internal/scanners/adf/adf.go +++ b/internal/scanners/adf/adf.go @@ -63,3 +63,7 @@ func (a *DataFactoryScanner) listFactories(resourceGroupName string) ([]*armdata } return factories, nil } + +func (a *DataFactoryScanner) ResourceType() string { + return "Microsoft.DataFactory/factories" +} diff --git a/internal/scanners/afd/afd.go b/internal/scanners/afd/afd.go index d32efd4a..583c79a2 100644 --- a/internal/scanners/afd/afd.go +++ b/internal/scanners/afd/afd.go @@ -63,3 +63,7 @@ func (a *FrontDoorScanner) list(resourceGroupName string) ([]*armcdn.Profile, er } return services, nil } + +func (a *FrontDoorScanner) ResourceType() string { + return "Microsoft.Cdn/profiles" +} diff --git a/internal/scanners/afw/afw.go b/internal/scanners/afw/afw.go index 7c4d3493..0c8ef745 100644 --- a/internal/scanners/afw/afw.go +++ b/internal/scanners/afw/afw.go @@ -38,13 +38,13 @@ func (a *FirewallScanner) Scan(resourceGroupName string, scanContext *scanners.S rr := engine.EvaluateRules(rules, g, scanContext) results = append(results, scanners.AzureServiceResult{ - SubscriptionID: a.config.SubscriptionID, + SubscriptionID: a.config.SubscriptionID, SubscriptionName: a.config.SubscriptionName, - ResourceGroup: resourceGroupName, - Location: *g.Location, - Type: *g.Type, - ServiceName: *g.Name, - Rules: rr, + ResourceGroup: resourceGroupName, + Location: *g.Location, + Type: *g.Type, + ServiceName: *g.Name, + Rules: rr, }) } return results, nil @@ -63,3 +63,7 @@ func (a *FirewallScanner) list(resourceGroupName string) ([]*armnetwork.AzureFir } return services, nil } + +func (a *FirewallScanner) ResourceType() string { + return "Microsoft.Network/azureFirewalls" +} diff --git a/internal/scanners/agw/agw.go b/internal/scanners/agw/agw.go index 483dd6bc..293f99fa 100644 --- a/internal/scanners/agw/agw.go +++ b/internal/scanners/agw/agw.go @@ -62,3 +62,7 @@ func (a *ApplicationGatewayScanner) listGateways(resourceGroupName string) ([]*a } return results, nil } + +func (a *ApplicationGatewayScanner) ResourceType() string { + return "Microsoft.Network/applicationGateways" +} diff --git a/internal/scanners/aks/aks.go b/internal/scanners/aks/aks.go index f4c00b46..64080b9f 100644 --- a/internal/scanners/aks/aks.go +++ b/internal/scanners/aks/aks.go @@ -65,3 +65,8 @@ func (a *AKSScanner) listClusters(resourceGroupName string) ([]*armcontainerserv } return clusters, nil } + +// GetRules - Returns the rules for the AKSScanner +func (a *AKSScanner) ResourceType() string { + return "Microsoft.ContainerService/managedClusters" +} diff --git a/internal/scanners/amg/amg.go b/internal/scanners/amg/amg.go index ea3d5aa8..ac9331ec 100644 --- a/internal/scanners/amg/amg.go +++ b/internal/scanners/amg/amg.go @@ -64,3 +64,7 @@ func (a *ManagedGrafanaScanner) listWorkspaces(resourceGroupName string) ([]*arm return workspaces, nil } + +func (a *ManagedGrafanaScanner) ResourceType() string { + return "Microsoft.Dashboard/grafana" +} diff --git a/internal/scanners/apim/apim.go b/internal/scanners/apim/apim.go index 82e50535..f7c2e79d 100644 --- a/internal/scanners/apim/apim.go +++ b/internal/scanners/apim/apim.go @@ -38,13 +38,13 @@ func (a *APIManagementScanner) Scan(resourceGroupName string, scanContext *scann rr := engine.EvaluateRules(rules, s, scanContext) results = append(results, scanners.AzureServiceResult{ - SubscriptionID: a.config.SubscriptionID, + SubscriptionID: a.config.SubscriptionID, SubscriptionName: a.config.SubscriptionName, - ResourceGroup: resourceGroupName, - ServiceName: *s.Name, - Type: *s.Type, - Location: *s.Location, - Rules: rr, + ResourceGroup: resourceGroupName, + ServiceName: *s.Name, + Type: *s.Type, + Location: *s.Location, + Rules: rr, }) } return results, nil @@ -63,3 +63,7 @@ func (a *APIManagementScanner) listServices(resourceGroupName string) ([]*armapi } return services, nil } + +func (a *APIManagementScanner) ResourceType() string { + return "Microsoft.ApiManagement/service" +} diff --git a/internal/scanners/appcs/appcs.go b/internal/scanners/appcs/appcs.go index 99d3bdd0..2b71718f 100644 --- a/internal/scanners/appcs/appcs.go +++ b/internal/scanners/appcs/appcs.go @@ -38,13 +38,13 @@ func (a *AppConfigurationScanner) Scan(resourceGroupName string, scanContext *sc rr := engine.EvaluateRules(rules, app, scanContext) results = append(results, scanners.AzureServiceResult{ - SubscriptionID: a.config.SubscriptionID, + SubscriptionID: a.config.SubscriptionID, SubscriptionName: a.config.SubscriptionName, - ResourceGroup: resourceGroupName, - ServiceName: *app.Name, - Type: *app.Type, - Location: *app.Location, - Rules: rr, + ResourceGroup: resourceGroupName, + ServiceName: *app.Name, + Type: *app.Type, + Location: *app.Location, + Rules: rr, }) } return results, nil @@ -62,3 +62,7 @@ func (a *AppConfigurationScanner) list(resourceGroupName string) ([]*armappconfi } return apps, nil } + +func (a *AppConfigurationScanner) ResourceType() string { + return "Microsoft.AppConfiguration/configurationStores" +} diff --git a/internal/scanners/appi/appi.go b/internal/scanners/appi/appi.go index 350f7d0e..f3aa31ef 100644 --- a/internal/scanners/appi/appi.go +++ b/internal/scanners/appi/appi.go @@ -63,3 +63,7 @@ func (a *AppInsightsScanner) list(resourceGroupName string) ([]*armapplicationin } return services, nil } + +func (a *AppInsightsScanner) ResourceType() string { + return "Microsoft.Insights/components" +} diff --git a/internal/scanners/as/as.go b/internal/scanners/as/as.go index a9f79fe6..dea96a25 100644 --- a/internal/scanners/as/as.go +++ b/internal/scanners/as/as.go @@ -63,3 +63,7 @@ func (c *AnalysisServicesScanner) listWorkspaces(resourceGroupName string) ([]*a } return registries, nil } + +func (a *AnalysisServicesScanner) ResourceType() string { + return "Microsoft.AnalysisServices/servers" +} diff --git a/internal/scanners/asp/asp.go b/internal/scanners/asp/asp.go index 607311e5..0d54f96f 100644 --- a/internal/scanners/asp/asp.go +++ b/internal/scanners/asp/asp.go @@ -146,3 +146,7 @@ func (a *AppServiceScanner) listSites(resourceGroupName string, plan string) ([] } return results, nil } + +func (a *AppServiceScanner) ResourceType() string { + return "Microsoft.Web/serverFarms" +} diff --git a/internal/scanners/ca/ca.go b/internal/scanners/ca/ca.go index a117d10c..f32653e6 100644 --- a/internal/scanners/ca/ca.go +++ b/internal/scanners/ca/ca.go @@ -62,3 +62,7 @@ func (a *ContainerAppsScanner) listApps(resourceGroupName string) ([]*armappcont } return apps, nil } + +func (a *ContainerAppsScanner) ResourceType() string { + return "Microsoft.App/containerApps" +} diff --git a/internal/scanners/cae/cae.go b/internal/scanners/cae/cae.go index e2cd7710..68f3edce 100644 --- a/internal/scanners/cae/cae.go +++ b/internal/scanners/cae/cae.go @@ -62,3 +62,7 @@ func (a *ContainerAppsEnvironmentScanner) listApps(resourceGroupName string) ([] } return apps, nil } + +func (a *ContainerAppsEnvironmentScanner) ResourceType() string { + return "Microsoft.App/managedenvironments" +} diff --git a/internal/scanners/ci/ci.go b/internal/scanners/ci/ci.go index b72cc581..cf4eeeb6 100644 --- a/internal/scanners/ci/ci.go +++ b/internal/scanners/ci/ci.go @@ -62,3 +62,7 @@ func (c *ContainerInstanceScanner) listInstances(resourceGroupName string) ([]*a } return apps, nil } + +func (a *ContainerInstanceScanner) ResourceType() string { + return "Microsoft.ContainerInstance/containerGroups" +} diff --git a/internal/scanners/cog/cog.go b/internal/scanners/cog/cog.go index cface3ca..bb73a912 100644 --- a/internal/scanners/cog/cog.go +++ b/internal/scanners/cog/cog.go @@ -63,3 +63,7 @@ func (c *CognitiveScanner) listEventHubs(resourceGroupName string) ([]*armcognit } return namespaces, nil } + +func (a *CognitiveScanner) ResourceType() string { + return "Microsoft.CognitiveServices/accounts" +} diff --git a/internal/scanners/cosmos/cosmos.go b/internal/scanners/cosmos/cosmos.go index 5fdbb559..d9e316ea 100644 --- a/internal/scanners/cosmos/cosmos.go +++ b/internal/scanners/cosmos/cosmos.go @@ -38,13 +38,13 @@ func (c *CosmosDBScanner) Scan(resourceGroupName string, scanContext *scanners.S rr := engine.EvaluateRules(rules, database, scanContext) results = append(results, scanners.AzureServiceResult{ - SubscriptionID: c.config.SubscriptionID, + SubscriptionID: c.config.SubscriptionID, SubscriptionName: c.config.SubscriptionName, - ResourceGroup: resourceGroupName, - ServiceName: *database.Name, - Type: *database.Type, - Location: *database.Location, - Rules: rr, + ResourceGroup: resourceGroupName, + ServiceName: *database.Name, + Type: *database.Type, + Location: *database.Location, + Rules: rr, }) } return results, nil @@ -63,3 +63,7 @@ func (c *CosmosDBScanner) listDatabases(resourceGroupName string) ([]*armcosmos. } return domains, nil } + +func (a *CosmosDBScanner) ResourceType() string { + return "Microsoft.DocumentDB/databaseAccounts" +} diff --git a/internal/scanners/cr/cr.go b/internal/scanners/cr/cr.go index a6fb865a..f7c9a073 100644 --- a/internal/scanners/cr/cr.go +++ b/internal/scanners/cr/cr.go @@ -38,13 +38,13 @@ func (c *ContainerRegistryScanner) Scan(resourceGroupName string, scanContext *s rr := engine.EvaluateRules(rules, registry, scanContext) results = append(results, scanners.AzureServiceResult{ - SubscriptionID: c.config.SubscriptionID, + SubscriptionID: c.config.SubscriptionID, SubscriptionName: c.config.SubscriptionName, - ResourceGroup: resourceGroupName, - ServiceName: *registry.Name, - Type: *registry.Type, - Location: *registry.Location, - Rules: rr, + ResourceGroup: resourceGroupName, + ServiceName: *registry.Name, + Type: *registry.Type, + Location: *registry.Location, + Rules: rr, }) } return results, nil @@ -63,3 +63,7 @@ func (c *ContainerRegistryScanner) listRegistries(resourceGroupName string) ([]* } return registries, nil } + +func (a *ContainerRegistryScanner) ResourceType() string { + return "Microsoft.ContainerRegistry/registries" +} diff --git a/internal/scanners/dbw/dbw.go b/internal/scanners/dbw/dbw.go index 488d5d5f..e98472ac 100644 --- a/internal/scanners/dbw/dbw.go +++ b/internal/scanners/dbw/dbw.go @@ -63,3 +63,7 @@ func (c *DatabricksScanner) listWorkspaces(resourceGroupName string) ([]*armdata } return registries, nil } + +func (a *DatabricksScanner) ResourceType() string { + return "Microsoft.Databricks/workspaces" +} diff --git a/internal/scanners/dec/dec.go b/internal/scanners/dec/dec.go index fc35817b..54011197 100644 --- a/internal/scanners/dec/dec.go +++ b/internal/scanners/dec/dec.go @@ -63,3 +63,7 @@ func (a *DataExplorerScanner) listClusters(resourceGroupName string) ([]*armkust } return kustoclusters, nil } + +func (a *DataExplorerScanner) ResourceType() string { + return "Microsoft.Kusto/clusters" +} diff --git a/internal/scanners/evgd/evgd.go b/internal/scanners/evgd/evgd.go index 81a5e5b6..68a9e18b 100644 --- a/internal/scanners/evgd/evgd.go +++ b/internal/scanners/evgd/evgd.go @@ -63,3 +63,7 @@ func (a *EventGridScanner) listDomain(resourceGroupName string) ([]*armeventgrid } return domains, nil } + +func (a *EventGridScanner) ResourceType() string { + return "Microsoft.EventGrid/domains" +} diff --git a/internal/scanners/evh/evh.go b/internal/scanners/evh/evh.go index 2d5cac36..f77023dc 100644 --- a/internal/scanners/evh/evh.go +++ b/internal/scanners/evh/evh.go @@ -63,3 +63,7 @@ func (c *EventHubScanner) listEventHubs(resourceGroupName string) ([]*armeventhu } return namespaces, nil } + +func (a *EventHubScanner) ResourceType() string { + return "Microsoft.EventHub/namespaces" +} diff --git a/internal/scanners/kv/kv.go b/internal/scanners/kv/kv.go index 2ac6064f..54bca3a8 100644 --- a/internal/scanners/kv/kv.go +++ b/internal/scanners/kv/kv.go @@ -63,3 +63,7 @@ func (c *KeyVaultScanner) listVaults(resourceGroupName string) ([]*armkeyvault.V } return vaults, nil } + +func (a *KeyVaultScanner) ResourceType() string { + return "Microsoft.KeyVault/vaults" +} diff --git a/internal/scanners/lb/lb.go b/internal/scanners/lb/lb.go index cf6b8a51..53d3f7e2 100644 --- a/internal/scanners/lb/lb.go +++ b/internal/scanners/lb/lb.go @@ -63,3 +63,7 @@ func (c *LoadBalancerScanner) list(resourceGroupName string) ([]*armnetwork.Load } return lbs, nil } + +func (a *LoadBalancerScanner) ResourceType() string { + return "Microsoft.Network/loadBalancers" +} diff --git a/internal/scanners/logic/logic.go b/internal/scanners/logic/logic.go index 60e21e2a..d1178c84 100644 --- a/internal/scanners/logic/logic.go +++ b/internal/scanners/logic/logic.go @@ -63,3 +63,7 @@ func (c *LogicAppScanner) list(resourceGroupName string) ([]*armlogic.Workflow, } return logicApps, nil } + +func (a *LogicAppScanner) ResourceType() string { + return "Microsoft.Logic/workflows" +} diff --git a/internal/scanners/maria/maria.go b/internal/scanners/maria/maria.go index 3ff411eb..49e2894a 100644 --- a/internal/scanners/maria/maria.go +++ b/internal/scanners/maria/maria.go @@ -103,3 +103,7 @@ func (c *MariaScanner) listDatabases(resourceGroupName, serverName string) ([]*a } return databases, nil } + +func (a *MariaScanner) ResourceType() string { + return "Microsoft.DBforMariaDB/servers" +} diff --git a/internal/scanners/mysql/mysql.go b/internal/scanners/mysql/mysql.go index f58a8e20..445822a5 100644 --- a/internal/scanners/mysql/mysql.go +++ b/internal/scanners/mysql/mysql.go @@ -64,3 +64,7 @@ func (c *MySQLScanner) listMySQL(resourceGroupName string) ([]*armmysql.Server, } return servers, nil } + +func (a *MySQLScanner) ResourceType() string { + return "Microsoft.DBforMySQL/servers" +} diff --git a/internal/scanners/mysql/mysqlf.go b/internal/scanners/mysql/mysqlf.go index ca934793..e3517ecc 100644 --- a/internal/scanners/mysql/mysqlf.go +++ b/internal/scanners/mysql/mysqlf.go @@ -63,3 +63,7 @@ func (c *MySQLFlexibleScanner) listFlexiblePostgre(resourceGroupName string) ([] } return servers, nil } + +func (a *MySQLFlexibleScanner) ResourceType() string { + return "Microsoft.DBforMySQL/flexibleServers" +} diff --git a/internal/scanners/psql/psql.go b/internal/scanners/psql/psql.go index d650625b..f212d590 100644 --- a/internal/scanners/psql/psql.go +++ b/internal/scanners/psql/psql.go @@ -64,3 +64,7 @@ func (c *PostgreScanner) listPostgre(resourceGroupName string) ([]*armpostgresql } return servers, nil } + +func (a *PostgreScanner) ResourceType() string { + return "Microsoft.DBforPostgreSQL/servers" +} diff --git a/internal/scanners/psql/psqlf.go b/internal/scanners/psql/psqlf.go index f31ca059..00aade36 100644 --- a/internal/scanners/psql/psqlf.go +++ b/internal/scanners/psql/psqlf.go @@ -50,6 +50,7 @@ func (c *PostgreFlexibleScanner) Scan(resourceGroupName string, scanContext *sca return results, nil } + func (c *PostgreFlexibleScanner) listFlexiblePostgre(resourceGroupName string) ([]*armpostgresqlflexibleservers.Server, error) { pager := c.flexibleClient.NewListByResourceGroupPager(resourceGroupName, nil) @@ -63,3 +64,7 @@ func (c *PostgreFlexibleScanner) listFlexiblePostgre(resourceGroupName string) ( } return servers, nil } + +func (a *PostgreFlexibleScanner) ResourceType() string { + return "Microsoft.DBforPostgreSQL/flexibleServers" +} diff --git a/internal/scanners/redis/redis.go b/internal/scanners/redis/redis.go index 3e00b351..fa274715 100644 --- a/internal/scanners/redis/redis.go +++ b/internal/scanners/redis/redis.go @@ -63,3 +63,7 @@ func (c *RedisScanner) listRedis(resourceGroupName string) ([]*armredis.Resource } return redis, nil } + +func (a *RedisScanner) ResourceType() string { + return "Microsoft.Cache/Redis" +} diff --git a/internal/scanners/sb/sb.go b/internal/scanners/sb/sb.go index eaf39c88..2dd2d089 100644 --- a/internal/scanners/sb/sb.go +++ b/internal/scanners/sb/sb.go @@ -63,3 +63,7 @@ func (c *ServiceBusScanner) listServiceBus(resourceGroupName string) ([]*armserv } return namespaces, nil } + +func (a *ServiceBusScanner) ResourceType() string { + return "Microsoft.ServiceBus/namespaces" +} diff --git a/internal/scanners/scanner.go b/internal/scanners/scanner.go index 8646fbf9..d6873c91 100644 --- a/internal/scanners/scanner.go +++ b/internal/scanners/scanner.go @@ -61,6 +61,7 @@ type ( Init(config *ScannerConfig) error GetRules() map[string]AzureRule Scan(resourceGroupName string, scanContext *ScanContext) ([]AzureServiceResult, error) + ResourceType() string } // AzureServiceResult - Struct for all Azure Service Results @@ -84,13 +85,58 @@ type ( } AzureRuleResult struct { - Id string - Category RulesCategory - Recommendation string - Impact ImpactType - Learn string - Result string - NotCompliant bool + RecommendationID string + Category RulesCategory + Recommendation string + Impact ImpactType + Learn string + Result string + NotCompliant bool + } + + AzureAprlRecommendation struct { + RecommendationID string `yaml:"aprlGuid"` + Recommendation string `yaml:"description"` + Category string `yaml:"recommendationControl"` + Impact string `yaml:"recommendationImpact"` + ResourceType string `yaml:"recommendationResourceType"` + MetadataState string `yaml:"recommendationMetadataState"` + LongDescription string `yaml:"longDescription"` + PotentialBenefits string `yaml:"potentialBenefits"` + PgVerified bool `yaml:"pgVerified"` + PublishedToLearn bool `yaml:"publishedToLearn"` + PublishedToAdvisor bool `yaml:"publishedToAdvisor"` + AutomationAvailable string `yaml:"automationAvailable"` + Tags string `yaml:"tags,omitempty"` + GraphQuery string `yaml:"graphQuery,omitempty"` + LearnMoreLink []struct { + Name string `yaml:"name"` + Url string `yaml:"url"` + } `yaml:"learnMoreLink,flow"` + } + + AzureServiceGraphRuleResult struct { + RecommendationID string + ResourceType string + Recommendation string + LongDescription string + PotentialBenefits string + ResourceID string + SubscriptionID string + SubscriptionName string + ResourceGroup string + Name string + Tags string + Category RulesCategory + Impact ImpactType + Learn string + Param1 string + Param2 string + Param3 string + Param4 string + Param5 string + AutomationAvailable string + Source string } RuleEngine struct{} @@ -156,13 +202,13 @@ func (e *RuleEngine) EvaluateRule(rule AzureRule, target interface{}, scanContex broken, result := rule.Eval(target, scanContext) return AzureRuleResult{ - Id: rule.Id, - Category: rule.Category, - Recommendation: rule.Recommendation, - Impact: rule.Impact, - Learn: rule.Url, - Result: result, - NotCompliant: broken, + RecommendationID: rule.Id, + Category: rule.Category, + Recommendation: rule.Recommendation, + Impact: rule.Impact, + Learn: rule.Url, + Result: result, + NotCompliant: broken, } } @@ -192,12 +238,23 @@ func MaskSubscriptionID(subscriptionID string, mask bool) string { return fmt.Sprintf("xxxxxxxx-xxxx-xxxx-xxxx-xxxxx%s", subscriptionID[29:]) } +func MaskSubscriptionIDInResourceID(resourceID string, mask bool) string { + if !mask { + return resourceID + } + + parts := strings.Split(resourceID, "/") + parts[2] = MaskSubscriptionID(parts[2], mask) + + return strings.Join(parts, "/") +} + func LogResourceGroupScan(subscriptionID string, resourceGroupName string, serviceName string) { log.Info().Msgf("Scanning subscriptions/...%s/resourceGroups/%s for %s", subscriptionID[29:], resourceGroupName, serviceName) } -func LogSubscriptionScan(subscriptionID string, serviceName string) { - log.Info().Msgf("Scanning subscriptions/...%s for %s", subscriptionID[29:], serviceName) +func LogSubscriptionScan(subscriptionID string, serviceTypeOrName string) { + log.Info().Msgf("Scanning subscriptions/...%s for %s", subscriptionID[29:], serviceTypeOrName) } type ImpactType string @@ -216,3 +273,55 @@ const ( RulesCategoryGovernance RulesCategory = "Governance" RulesCategoryOtherBestPractices RulesCategory = "Other Best Practices" ) + +// GetGraphRules - Get Graph Rules for a service type +func GetGraphRules(service string, aprl map[string]map[string]AzureAprlRecommendation) map[string]AzureAprlRecommendation { + r := map[string]AzureAprlRecommendation{} + if i, ok := aprl[strings.ToLower(service)]; ok { + for _, recommendation := range i { + if strings.Contains(recommendation.GraphQuery, "cannot-be-validated-with-arg") || + strings.Contains(recommendation.GraphQuery, "under-development") || + strings.Contains(recommendation.GraphQuery, "under development") { + continue + } + + r[recommendation.RecommendationID] = recommendation + } + } + return r +} + +// GetSubsctiptionFromResourceID - Get Subscription ID from Resource ID +func GetSubsctiptionFromResourceID(resourceID string) string { + parts := strings.Split(resourceID, "/") + return parts[2] +} + +// GetResourceGroupFromResourceID - Get Resource Group from Resource ID +func GetResourceGroupFromResourceID(resourceID string) string { + parts := strings.Split(resourceID, "/") + return parts[4] +} + +func (r *AzureRule) ToAzureAprlRecommendation(resourceType string) AzureAprlRecommendation { + return AzureAprlRecommendation{ + RecommendationID: r.Id, + Recommendation: r.Recommendation, + Category: string(r.Category), + Impact: string(r.Impact), + ResourceType: resourceType, + MetadataState: "", + LongDescription: r.Recommendation, + PotentialBenefits: "", + PgVerified: false, + PublishedToLearn: false, + PublishedToAdvisor: false, + AutomationAvailable: "", + Tags: "", + GraphQuery: "", + LearnMoreLink: []struct { + Name string "yaml:\"name\"" + Url string "yaml:\"url\"" + }{{Name: "Learn More", Url: r.Url}}, + } +} diff --git a/internal/scanners/sigr/sigr.go b/internal/scanners/sigr/sigr.go index 51975645..dc762e5b 100644 --- a/internal/scanners/sigr/sigr.go +++ b/internal/scanners/sigr/sigr.go @@ -63,3 +63,7 @@ func (c *SignalRScanner) listSignalR(resourceGroupName string) ([]*armsignalr.Re } return signalrs, nil } + +func (a *SignalRScanner) ResourceType() string { + return "Microsoft.SignalRService/SignalR" +} diff --git a/internal/scanners/sql/sql.go b/internal/scanners/sql/sql.go index bf388155..9aa0d1ad 100644 --- a/internal/scanners/sql/sql.go +++ b/internal/scanners/sql/sql.go @@ -148,3 +148,7 @@ func (c *SQLScanner) listPools(resourceGroupName, serverName string) ([]*armsql. } return pools, nil } + +func (a *SQLScanner) ResourceType() string { + return "Microsoft.Sql/servers" +} diff --git a/internal/scanners/st/st.go b/internal/scanners/st/st.go index be9413d7..c4b8fc53 100644 --- a/internal/scanners/st/st.go +++ b/internal/scanners/st/st.go @@ -74,3 +74,7 @@ func (c *StorageScanner) listStorage(resourceGroupName string) ([]*armstorage.Ac } return staccounts, nil } + +func (a *StorageScanner) ResourceType() string { + return "Microsoft.Storage/storageAccounts" +} diff --git a/internal/scanners/synw/synw.go b/internal/scanners/synw/synw.go index 6a7b54a8..a92dffd1 100644 --- a/internal/scanners/synw/synw.go +++ b/internal/scanners/synw/synw.go @@ -147,3 +147,7 @@ func (a *SynapseWorkspaceScanner) listSparkPools(resourceGroupName string, works } return results, nil } + +func (a *SynapseWorkspaceScanner) ResourceType() string { + return "Microsoft.Synapse/workspaces" +} diff --git a/internal/scanners/traf/traf.go b/internal/scanners/traf/traf.go index 90efedb5..303e81b5 100644 --- a/internal/scanners/traf/traf.go +++ b/internal/scanners/traf/traf.go @@ -38,13 +38,13 @@ func (c *TrafficManagerScanner) Scan(resourceGroupName string, scanContext *scan rr := engine.EvaluateRules(rules, w, scanContext) results = append(results, scanners.AzureServiceResult{ - SubscriptionID: c.config.SubscriptionID, + SubscriptionID: c.config.SubscriptionID, SubscriptionName: c.config.SubscriptionName, - ResourceGroup: resourceGroupName, - ServiceName: *w.Name, - Type: *w.Type, - Location: *w.Location, - Rules: rr, + ResourceGroup: resourceGroupName, + ServiceName: *w.Name, + Type: *w.Type, + Location: *w.Location, + Rules: rr, }) } return results, nil @@ -63,3 +63,7 @@ func (c *TrafficManagerScanner) list(resourceGroupName string) ([]*armtrafficman } return vnets, nil } + +func (a *TrafficManagerScanner) ResourceType() string { + return "Microsoft.Network/trafficManagerProfiles" +} diff --git a/internal/scanners/vgw/vgw.go b/internal/scanners/vgw/vgw.go index 2a761285..556f8a25 100644 --- a/internal/scanners/vgw/vgw.go +++ b/internal/scanners/vgw/vgw.go @@ -63,3 +63,7 @@ func (c *VirtualNetworkGatewayScanner) listVirtualNetworkGateways(resourceGroupN } return vpns, nil } + +func (a *VirtualNetworkGatewayScanner) ResourceType() string { + return "Microsoft.Network/virtualNetworkGateways" +} diff --git a/internal/scanners/vm/vm.go b/internal/scanners/vm/vm.go index 1c8f9ca2..03299d9c 100644 --- a/internal/scanners/vm/vm.go +++ b/internal/scanners/vm/vm.go @@ -63,3 +63,7 @@ func (c *VirtualMachineScanner) list(resourceGroupName string) ([]*armcompute.Vi } return vms, nil } + +func (a *VirtualMachineScanner) ResourceType() string { + return "Microsoft.Compute/virtualMachines" +} diff --git a/internal/scanners/vmss/vmss.go b/internal/scanners/vmss/vmss.go index 5a3f8792..83ccdd97 100644 --- a/internal/scanners/vmss/vmss.go +++ b/internal/scanners/vmss/vmss.go @@ -63,3 +63,7 @@ func (c *VirtualMachineScaleSetScanner) list(resourceGroupName string) ([]*armco } return vmss, nil } + +func (a *VirtualMachineScaleSetScanner) ResourceType() string { + return "Microsoft.Compute/virtualMachineScaleSets" +} diff --git a/internal/scanners/vnet/vnet.go b/internal/scanners/vnet/vnet.go index 20c7279d..4086cf87 100644 --- a/internal/scanners/vnet/vnet.go +++ b/internal/scanners/vnet/vnet.go @@ -63,3 +63,7 @@ func (c *VirtualNetworkScanner) list(resourceGroupName string) ([]*armnetwork.Vi } return vnets, nil } + +func (a *VirtualNetworkScanner) ResourceType() string { + return "Microsoft.Network/virtualNetworks" +} diff --git a/internal/scanners/vwan/vwan.go b/internal/scanners/vwan/vwan.go index a7979817..6ad97177 100644 --- a/internal/scanners/vwan/vwan.go +++ b/internal/scanners/vwan/vwan.go @@ -63,3 +63,7 @@ func (c *VirtualWanScanner) list(resourceGroupName string) ([]*armnetwork.Virtua } return vwans, nil } + +func (a *VirtualWanScanner) ResourceType() string { + return "Microsoft.Network/virtualWans" +} diff --git a/internal/scanners/wps/wps.go b/internal/scanners/wps/wps.go index 14dbf45e..03b99de8 100644 --- a/internal/scanners/wps/wps.go +++ b/internal/scanners/wps/wps.go @@ -63,3 +63,7 @@ func (c *WebPubSubScanner) listWebPubSub(resourceGroupName string) ([]*armwebpub } return WebPubSubs, nil } + +func (c *WebPubSubScanner) ResourceType() string { + return "Microsoft.SignalRService/webPubSub" +}