internal/scanner.go (309 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package internal
import (
"context"
"fmt"
"strings"
"time"
"github.com/Azure/azqr/internal/graph"
"github.com/Azure/azqr/internal/models"
"github.com/Azure/azqr/internal/renderers"
"github.com/Azure/azqr/internal/renderers/csv"
"github.com/Azure/azqr/internal/renderers/excel"
"github.com/Azure/azqr/internal/renderers/json"
"github.com/Azure/azqr/internal/scanners"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
_ "github.com/Azure/azqr/internal/scanners/aa"
_ "github.com/Azure/azqr/internal/scanners/adf"
_ "github.com/Azure/azqr/internal/scanners/afd"
_ "github.com/Azure/azqr/internal/scanners/afw"
_ "github.com/Azure/azqr/internal/scanners/agw"
_ "github.com/Azure/azqr/internal/scanners/aks"
_ "github.com/Azure/azqr/internal/scanners/amg"
_ "github.com/Azure/azqr/internal/scanners/apim"
_ "github.com/Azure/azqr/internal/scanners/appcs"
_ "github.com/Azure/azqr/internal/scanners/appi"
_ "github.com/Azure/azqr/internal/scanners/as"
_ "github.com/Azure/azqr/internal/scanners/asp"
_ "github.com/Azure/azqr/internal/scanners/avail"
_ "github.com/Azure/azqr/internal/scanners/avd"
_ "github.com/Azure/azqr/internal/scanners/avs"
_ "github.com/Azure/azqr/internal/scanners/ba"
_ "github.com/Azure/azqr/internal/scanners/ca"
_ "github.com/Azure/azqr/internal/scanners/cae"
_ "github.com/Azure/azqr/internal/scanners/ci"
_ "github.com/Azure/azqr/internal/scanners/cog"
_ "github.com/Azure/azqr/internal/scanners/conn"
_ "github.com/Azure/azqr/internal/scanners/cosmos"
_ "github.com/Azure/azqr/internal/scanners/cr"
_ "github.com/Azure/azqr/internal/scanners/dbw"
_ "github.com/Azure/azqr/internal/scanners/dec"
_ "github.com/Azure/azqr/internal/scanners/disk"
_ "github.com/Azure/azqr/internal/scanners/erc"
_ "github.com/Azure/azqr/internal/scanners/evgd"
_ "github.com/Azure/azqr/internal/scanners/evh"
_ "github.com/Azure/azqr/internal/scanners/fdfp"
_ "github.com/Azure/azqr/internal/scanners/gal"
_ "github.com/Azure/azqr/internal/scanners/hpc"
_ "github.com/Azure/azqr/internal/scanners/iot"
_ "github.com/Azure/azqr/internal/scanners/it"
_ "github.com/Azure/azqr/internal/scanners/kv"
_ "github.com/Azure/azqr/internal/scanners/lb"
_ "github.com/Azure/azqr/internal/scanners/log"
_ "github.com/Azure/azqr/internal/scanners/logic"
_ "github.com/Azure/azqr/internal/scanners/maria"
_ "github.com/Azure/azqr/internal/scanners/mysql"
_ "github.com/Azure/azqr/internal/scanners/netapp"
_ "github.com/Azure/azqr/internal/scanners/ng"
_ "github.com/Azure/azqr/internal/scanners/nic"
_ "github.com/Azure/azqr/internal/scanners/nsg"
_ "github.com/Azure/azqr/internal/scanners/nw"
_ "github.com/Azure/azqr/internal/scanners/pdnsz"
_ "github.com/Azure/azqr/internal/scanners/pep"
_ "github.com/Azure/azqr/internal/scanners/pip"
_ "github.com/Azure/azqr/internal/scanners/psql"
_ "github.com/Azure/azqr/internal/scanners/redis"
_ "github.com/Azure/azqr/internal/scanners/rg"
_ "github.com/Azure/azqr/internal/scanners/rsv"
_ "github.com/Azure/azqr/internal/scanners/rt"
_ "github.com/Azure/azqr/internal/scanners/sap"
_ "github.com/Azure/azqr/internal/scanners/sb"
_ "github.com/Azure/azqr/internal/scanners/sigr"
_ "github.com/Azure/azqr/internal/scanners/sql"
_ "github.com/Azure/azqr/internal/scanners/st"
_ "github.com/Azure/azqr/internal/scanners/synw"
_ "github.com/Azure/azqr/internal/scanners/traf"
_ "github.com/Azure/azqr/internal/scanners/vdpool"
_ "github.com/Azure/azqr/internal/scanners/vgw"
_ "github.com/Azure/azqr/internal/scanners/vm"
_ "github.com/Azure/azqr/internal/scanners/vmss"
_ "github.com/Azure/azqr/internal/scanners/vnet"
_ "github.com/Azure/azqr/internal/scanners/vwan"
_ "github.com/Azure/azqr/internal/scanners/wps"
)
type (
ScanParams struct {
ManagementGroupID string
SubscriptionID string
ResourceGroup string
OutputName string
Defender bool
Advisor bool
Cost bool
Mask bool
Csv bool
Json bool
Debug bool
ScannerKeys []string
ForceAzureCliCredential bool
Filters *models.Filters
UseAzqrRecommendations bool
UseAprlRecommendations bool
}
Scanner struct{}
)
func NewScanParams() *ScanParams {
return &ScanParams{
SubscriptionID: "",
ResourceGroup: "",
OutputName: "",
Defender: true,
Advisor: true,
Cost: true,
Mask: true,
Csv: false,
Json: false,
Debug: false,
ScannerKeys: []string{},
ForceAzureCliCredential: false,
Filters: models.NewFilters(),
UseAzqrRecommendations: true,
UseAprlRecommendations: true,
}
}
func (sc Scanner) Scan(params *ScanParams) {
// Default level for this example is info, unless debug flag is present
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if params.Debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
log.Debug().Msg("Debug logging enabled")
}
// generate output file name
outputFile := sc.generateOutputFileName(params.OutputName)
// load filters
filters := params.Filters
// validate input
if params.ManagementGroupID != "" && (params.SubscriptionID != "" || params.ResourceGroup != "") {
log.Fatal().Msg("Management Group name cannot be used with a Subscription Id or Resource Group name")
}
if params.SubscriptionID == "" && params.ResourceGroup != "" {
log.Fatal().Msg("Resource Group name can only be used with a Subscription Id")
}
if params.SubscriptionID != "" {
filters.Azqr.AddSubscription(params.SubscriptionID)
}
if params.ResourceGroup != "" {
filters.Azqr.AddResourceGroup(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", params.SubscriptionID, params.ResourceGroup))
}
serviceScanners := filters.Azqr.Scanners
// create Azure credentials
cred := sc.newAzureCredential(params.ForceAzureCliCredential)
// create a cancelable context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// create ARM client options
clientOptions := &arm.ClientOptions{
ClientOptions: policy.ClientOptions{
Retry: policy.RetryOptions{
RetryDelay: 20 * time.Millisecond,
MaxRetries: 3,
MaxRetryDelay: 10 * time.Minute,
},
},
}
// list subscriptions. Key is subscription ID, value is subscription name
var subscriptions map[string]string
if params.ManagementGroupID != "" {
managementGroupScanner := scanners.ManagementGroupsScanner{}
subscriptions = managementGroupScanner.ListSubscriptions(ctx, cred, params.ManagementGroupID, filters, clientOptions)
} else {
subscriptionScanner := scanners.SubcriptionScanner{}
subscriptions = subscriptionScanner.ListSubscriptions(ctx, cred, params.SubscriptionID, filters, clientOptions)
}
// initialize scanners
defenderScanner := scanners.DefenderScanner{}
pipScanner := scanners.PublicIPScanner{}
peScanner := scanners.PrivateEndpointScanner{}
diagnosticsScanner := scanners.DiagnosticSettingsScanner{}
advisorScanner := scanners.AdvisorScanner{}
costScanner := scanners.CostScanner{}
diagResults := map[string]bool{}
// initialize report data
reportData := renderers.NewReportData(outputFile, params.Mask)
// get the APRL scan results
aprlScanner := graph.NewAprlScanner(serviceScanners, filters, subscriptions)
reportData.Recommendations, reportData.Aprl = aprlScanner.Scan(ctx, cred)
resourceScanner := scanners.ResourceScanner{}
reportData.Resources, reportData.ExludedResources = resourceScanner.GetAllResources(ctx, cred, subscriptions, filters)
// For each service scanner, get the recommendations list
if params.UseAzqrRecommendations {
for _, s := range serviceScanners {
for i, r := range s.GetRecommendations() {
if filters.Azqr.IsRecommendationExcluded(r.RecommendationID) {
continue
}
if r.RecommendationType != models.TypeRecommendation {
continue
}
if reportData.Recommendations[strings.ToLower(r.ResourceType)] == nil {
reportData.Recommendations[strings.ToLower(r.ResourceType)] = map[string]models.AprlRecommendation{}
}
reportData.Recommendations[strings.ToLower(r.ResourceType)][i] = r.ToAzureAprlRecommendation()
}
}
// scan diagnostic settings
err := diagnosticsScanner.Init(ctx, cred, clientOptions)
if err != nil {
log.Fatal().Err(err).Msg("Failed to initialize diagnostic settings scanner")
}
diagResults = diagnosticsScanner.Scan(reportData.ResourceIDs())
}
// scan each subscription with AZQR scanners
for sid, sn := range subscriptions {
config := &models.ScannerConfig{
Ctx: ctx,
SubscriptionID: sid,
SubscriptionName: sn,
Cred: cred,
ClientOptions: clientOptions,
}
if params.UseAzqrRecommendations {
// scan private endpoints
peResults := peScanner.Scan(config)
// scan public IPs
pips := pipScanner.Scan(config)
// initialize scan context
scanContext := models.ScanContext{
Filters: filters,
PrivateEndpoints: peResults,
DiagnosticsSettings: diagResults,
PublicIPs: pips,
}
// scan each resource group
ch := make(chan []models.AzqrServiceResult, len(serviceScanners))
for _, s := range serviceScanners {
err := s.Init(config)
if err != nil {
log.Fatal().Err(err).Msg("Failed to initialize scanner")
}
go func(s models.IAzureScanner) {
res, err := sc.retry(3, 10*time.Millisecond, s, &scanContext)
if err != nil {
cancel()
log.Fatal().Err(err).Msg("Failed to scan")
}
ch <- res
}(s)
}
for i := 0; i < len(serviceScanners); i++ {
res := <-ch
for _, r := range res {
// check if the resource is excluded
if filters.Azqr.IsServiceExcluded(r.ResourceID()) {
continue
}
reportData.Azqr = append(reportData.Azqr, r)
}
}
}
// scan costs
costs := costScanner.Scan(params.Cost, config)
reportData.Cost.From = costs.From
reportData.Cost.To = costs.To
reportData.Cost.Items = append(reportData.Cost.Items, costs.Items...)
}
// get the count of resources per resource type
reportData.ResourceTypeCount = resourceScanner.GetCountPerResourceType(ctx, cred, subscriptions, reportData.Recommendations, filters)
// scan advisor
reportData.Advisor = append(reportData.Advisor, advisorScanner.Scan(ctx, params.Defender, cred, subscriptions, filters)...)
// scan defender
reportData.Defender = append(reportData.Defender, defenderScanner.Scan(ctx, params.Defender, cred, subscriptions, filters)...)
// get the defender recommendations
reportData.DefenderRecommendations = append(reportData.DefenderRecommendations, defenderScanner.GetRecommendations(ctx, params.Defender, cred, subscriptions, filters)...)
// render excel report
excel.CreateExcelReport(&reportData)
// render json report
if params.Json {
json.CreateJsonReport(&reportData)
}
// render csv reports
if params.Csv {
csv.CreateCsvReport(&reportData)
}
log.Info().Msg("Scan completed.")
}
// retry retries the Azure scanner Scan, a number of times with an increasing delay between retries
func (sc Scanner) retry(attempts int, sleep time.Duration, a models.IAzureScanner, scanContext *models.ScanContext) ([]models.AzqrServiceResult, error) {
var err error
for i := 0; ; i++ {
res, err := a.Scan(scanContext)
if err == nil {
return res, nil
}
if models.ShouldSkipError(err) {
return []models.AzqrServiceResult{}, 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 nil, err
}
func (sc Scanner) newAzureCredential(forceAzureCliCredential bool) azcore.TokenCredential {
var cred azcore.TokenCredential
var err error
if !forceAzureCliCredential {
cred, err = azidentity.NewDefaultAzureCredential(nil)
if err != nil {
log.Fatal().Err(err).Msg("Failed to get Azure credentials")
}
} else {
cred, err = azidentity.NewAzureCLICredential(nil)
if err != nil {
log.Fatal().Err(err).Msg("Failed to get Azure CLI credentials")
}
}
return cred
}
func (sc Scanner) generateOutputFileName(outputName string) string {
outputFile := outputName
if outputFile == "" {
current_time := time.Now()
outputFileStamp := fmt.Sprintf("%d_%02d_%02d_T%02d%02d%02d",
current_time.Year(), current_time.Month(), current_time.Day(),
current_time.Hour(), current_time.Minute(), current_time.Second())
outputFile = fmt.Sprintf("%s_%s", "azqr_action_plan", outputFileStamp)
}
return outputFile
}